@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.
- package/.dockerignore +24 -0
- package/.env.example +26 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +39 -0
- package/CONTRIBUTING.md +73 -0
- package/Dockerfile +16 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/SECURITY.md +26 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/bin/ezenciel-agents +2 -0
- package/bin/ezenciel-agents-ai +2 -0
- package/bin/ezenciel-agents-ai.mjs +5 -0
- package/bin/ezenciel-agents-approval +2 -0
- package/bin/ezenciel-agents-approval.mjs +16 -0
- package/bin/ezenciel-agents-create +12 -0
- package/bin/ezenciel-agents-docker +6 -0
- package/bin/ezenciel-agents-host +5 -0
- package/bin/ezenciel-agents-install +2 -0
- package/bin/ezenciel-agents-message +2 -0
- package/bin/ezenciel-agents-message.mjs +16 -0
- package/bin/ezenciel-agents-owner +2 -0
- package/bin/ezenciel-agents-owner.mjs +18 -0
- package/bin/ezenciel-agents-react +2 -0
- package/bin/ezenciel-agents-react.mjs +16 -0
- package/bin/ezenciel-agents-setup.mjs +18 -0
- package/bin/ezenciel-agents-source +2 -0
- package/bin/ezenciel-agents-source.mjs +16 -0
- package/bin/ezenciel-agents-tools.mjs +3 -0
- package/bin/ezenciel-agents.mjs +37 -0
- package/compose.whatsapp.yaml +12 -0
- package/compose.yaml +40 -0
- package/default-plugins.json +1 -0
- package/docker/entrypoint.sh +15 -0
- package/docker/healthcheck.mjs +8 -0
- package/docker/plugin-smoke.mjs +48 -0
- package/docker/pnpm-lock.yaml +415 -0
- package/docker/recovery.ts +11 -0
- package/docker/run.ts +52 -0
- package/docker/smoke.mjs +47 -0
- package/docker/status-smoke.mjs +30 -0
- package/docker/upgrade-smoke.mjs +58 -0
- package/docs/architecture/ai-selection.md +37 -0
- package/docs/architecture/authority-boundaries.md +14 -0
- package/docs/architecture/event-sources.md +34 -0
- package/docs/architecture/telegram-intake.md +29 -0
- package/docs/development-and-testing.md +18 -0
- package/docs/docker-runtime.md +118 -0
- package/docs/host-service.md +80 -0
- package/docs/plugin-contributions.md +34 -0
- package/docs/plugins.md +181 -0
- package/docs/releasing.md +71 -0
- package/docs/setup.md +234 -0
- package/docs/upgrades.md +193 -0
- package/package.json +106 -0
- package/scripts/assert-local-registry.mjs +22 -0
- package/scripts/release-check.mjs +14 -0
- package/scripts/smoke.ts +102 -0
- package/src/agent-install.ts +96 -0
- package/src/ai-cli.ts +22 -0
- package/src/ai.ts +88 -0
- package/src/approval-cli.ts +59 -0
- package/src/approval.ts +119 -0
- package/src/audio.ts +184 -0
- package/src/client-defaults.ts +101 -0
- package/src/config.ts +48 -0
- package/src/control-state.ts +350 -0
- package/src/desktop-bridge.ts +284 -0
- package/src/event-sources.ts +112 -0
- package/src/executor.ts +335 -0
- package/src/files.ts +75 -0
- package/src/format.ts +57 -0
- package/src/host-executor-client.ts +46 -0
- package/src/host-executor-protocol.ts +2 -0
- package/src/host-executor.ts +129 -0
- package/src/identity.ts +17 -0
- package/src/inbox.ts +171 -0
- package/src/index.ts +812 -0
- package/src/install-config.ts +56 -0
- package/src/install-tools.mjs +98 -0
- package/src/menu.ts +123 -0
- package/src/message-send.ts +57 -0
- package/src/message.ts +68 -0
- package/src/owner-args.ts +4 -0
- package/src/owner.ts +30 -0
- package/src/plugins/manager.mjs +272 -0
- package/src/react.ts +33 -0
- package/src/reaction.ts +32 -0
- package/src/read-request.ts +72 -0
- package/src/reply.ts +13 -0
- package/src/runs.ts +445 -0
- package/src/service.ts +28 -0
- package/src/setup.ts +180 -0
- package/src/software-status.ts +23 -0
- package/src/source-cli.ts +18 -0
- package/src/update-attention.ts +18 -0
- package/src/updates/artifact.mjs +83 -0
- package/src/updates/binding.mjs +27 -0
- package/src/updates/control.mjs +131 -0
- package/src/updates/launch.mjs +13 -0
- package/src/updates/runtime.mjs +140 -0
- package/src/updates/status.mjs +49 -0
- package/src/updates/supervisor.mjs +102 -0
- package/src/version.ts +4 -0
- package/src/workspace.ts +32 -0
- package/templates/agent/AGENTS.md +49 -0
- package/templates/agent/SOUL.md +11 -0
- package/templates/agent/TOOLS.md +46 -0
- package/templates/agent/USER.md +5 -0
- package/templates/updates.md +45 -0
- package/test/agent-install.test.ts +48 -0
- package/test/ai-cli.test.ts +28 -0
- package/test/ai.test.ts +105 -0
- package/test/approval.test.ts +40 -0
- package/test/audio.test.ts +77 -0
- package/test/client-defaults.test.ts +64 -0
- package/test/codex-context.test.ts +33 -0
- package/test/config.test.ts +32 -0
- package/test/control-state.test.ts +58 -0
- package/test/desktop-bridge.test.ts +159 -0
- package/test/docker-runtime.test.ts +23 -0
- package/test/event-sources.test.ts +113 -0
- package/test/executor.test.ts +135 -0
- package/test/files.test.ts +50 -0
- package/test/format.test.ts +41 -0
- package/test/host-executor.test.ts +149 -0
- package/test/inbox-burst.test.ts +66 -0
- package/test/inbox.test.ts +102 -0
- package/test/install-config.test.ts +75 -0
- package/test/install-tools.test.mjs +59 -0
- package/test/intake-relay.test.ts +337 -0
- package/test/owner-help.test.mjs +10 -0
- package/test/plugin-manager.test.mjs +157 -0
- package/test/publish-guard.test.ts +21 -0
- package/test/reaction.test.ts +122 -0
- package/test/read-request.test.ts +134 -0
- package/test/relay.test.ts +254 -0
- package/test/release-entrypoints.test.mjs +26 -0
- package/test/runs.test.ts +116 -0
- package/test/security.test.ts +85 -0
- package/test/setup.test.ts +68 -0
- package/test/software-status.test.ts +31 -0
- package/test/update-attention.test.ts +21 -0
- package/test/updates.test.mjs +282 -0
- package/test/upgrade-pause.test.ts +70 -0
- package/test/workspace.test.ts +80 -0
- package/tsconfig.json +19 -0
package/docker/smoke.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
const image = process.env.EZ_RELAY_IMAGE || 'ezenciel-agents:local';
|
|
7
|
+
const dir = mkdtempSync(join(tmpdir(), 'ez-docker-qa-'));
|
|
8
|
+
const holder = `ez-main-lock-qa-${process.pid}`;
|
|
9
|
+
const volume = `${holder}-control`;
|
|
10
|
+
const marker = 'qa-private-secret-never-in-executor';
|
|
11
|
+
writeFileSync(join(dir, 'relay.env'), `TELEGRAM_BOT_TOKEN=${marker}\n`, { mode: 0o600 });
|
|
12
|
+
const run = (args) => spawnSync('docker', args, { encoding: 'utf8', timeout: 60000 });
|
|
13
|
+
try {
|
|
14
|
+
const probe = `
|
|
15
|
+
const fs = require('fs'), assert = require('assert/strict');
|
|
16
|
+
assert.equal(process.getuid(), 1000);
|
|
17
|
+
assert.equal(process.env.TELEGRAM_BOT_TOKEN, undefined);
|
|
18
|
+
assert.throws(() => fs.openSync('/proc/'+process.ppid+'/mem', 'r'), {code:'EACCES'});
|
|
19
|
+
assert.throws(() => fs.readFileSync('/run/secrets/relay_env'), {code:'EACCES'});
|
|
20
|
+
assert.equal(fs.existsSync('/var/run/docker.sock'), false);
|
|
21
|
+
for (const pid of ['1', String(process.ppid)]) {
|
|
22
|
+
try { assert.ok(!fs.readFileSync('/proc/'+pid+'/environ','utf8').includes('${marker}')); }
|
|
23
|
+
catch (e) { if (e.code !== 'EACCES') throw e; }
|
|
24
|
+
}
|
|
25
|
+
console.log(JSON.stringify({uid:process.getuid(), argv:process.argv.slice(1), private:true}));
|
|
26
|
+
`;
|
|
27
|
+
const literal = 'space $(touch /tmp/ez-should-not-exist); `echo no`';
|
|
28
|
+
const result = run(['run', '--rm', '--mount', `type=bind,src=${join(dir,'relay.env')},dst=/run/secrets/relay_env,readonly`, image, 'exec', 'node', '-e', probe, literal]);
|
|
29
|
+
assert.equal(result.status, 0, result.stderr);
|
|
30
|
+
assert.deepEqual(JSON.parse(result.stdout).argv, [literal]);
|
|
31
|
+
const failed = run(['run','--rm',image,'exec','node','-e','process.exit(23)']);
|
|
32
|
+
assert.equal(failed.status, 23, failed.stderr);
|
|
33
|
+
const held = run(['run','-d','--name',holder,'-v',`${volume}:/state/control`,image,'exec','node','-e',"require('fs').writeFileSync('/state/control/ready','yes');setInterval(()=>{},1000)"]);
|
|
34
|
+
assert.equal(held.status,0,held.stderr);
|
|
35
|
+
for (let n=0;n<30;n++) {
|
|
36
|
+
const probe=run(['exec',holder,'test','-f','/state/control/ready']);
|
|
37
|
+
if(probe.status===0)break;
|
|
38
|
+
await new Promise(resolve=>setTimeout(resolve,100));
|
|
39
|
+
}
|
|
40
|
+
assert.equal(run(['exec',holder,'test','-f','/state/control/ready']).status,0);
|
|
41
|
+
const duplicate=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
|
|
42
|
+
assert.equal(duplicate.status,73,duplicate.stderr);
|
|
43
|
+
assert.equal(run(['kill',holder]).status,0);
|
|
44
|
+
const recovered=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
|
|
45
|
+
assert.equal(recovered.status,0,recovered.stderr);
|
|
46
|
+
console.log('Docker smoke passed: non-root executor, private secret mount/environment, literal argv, exit code, no Docker socket, duplicate writer rejection and crash lock release.');
|
|
47
|
+
} finally { run(['rm','-f',holder]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {randomUUID} from 'node:crypto';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import {status} from '../src/updates/status.mjs';
|
|
8
|
+
import {execute} from '../src/updates/runtime.mjs';
|
|
9
|
+
|
|
10
|
+
const root=await fs.realpath(await fs.mkdtemp(path.join(tmpdir(),'ez-status-smoke-')));
|
|
11
|
+
const home=path.join(root,'tools'),workspace=path.join(root,'mind'),controlDir=path.join(root,'control');
|
|
12
|
+
const project='ez-status-'+randomUUID().slice(0,8),compose=path.join(root,'compose.json');
|
|
13
|
+
const image=process.env.EZ_RELAY_IMAGE||'ezenciel-agents:local';
|
|
14
|
+
const docker=args=>execute('docker',['compose','--project-name',project,'--file',compose,...args]);
|
|
15
|
+
const write=(file,value)=>fs.writeFile(file,JSON.stringify(value),{mode:0o600});
|
|
16
|
+
try {
|
|
17
|
+
for(const dir of [home,workspace,controlDir])await fs.mkdir(dir,{mode:0o700});
|
|
18
|
+
await write(path.join(home,'config.json'),{schemaVersion:1,workspace,deploymentDir:root,packageRoot:fileURLToPath(new URL('../',import.meta.url))});
|
|
19
|
+
await write(path.join(root,'host-executor.json'),{agents:[{workspace,controlDir,toolsHome:home}]});
|
|
20
|
+
await write(path.join(home,'registry.json'),{plugins:{sample:{manifest:{id:'sample',version:'0.1.2'},project,compose,deployment:{services:{sample:{image}}}}}});
|
|
21
|
+
await write(compose,{services:{sample:{image,entrypoint:['node','-e','setInterval(()=>{},1000)'],user:'1000:1000',network_mode:'none',cap_drop:['ALL']}}});
|
|
22
|
+
await docker(['up','-d']);
|
|
23
|
+
let p=(await status(home)).plugins[0];
|
|
24
|
+
assert.equal(p.state,'running');assert.equal(p.runningVersion,'0.1.2');assert.equal(p.services[0].imageMatches,true);
|
|
25
|
+
await docker(['stop']);p=(await status(home)).plugins[0];
|
|
26
|
+
assert.equal(p.state,'stopped');assert.equal(p.installedVersion,'0.1.2');assert.equal(p.runningVersion,null);
|
|
27
|
+
console.log('Status Docker smoke passed: running image identity and stopped plugin versions.');
|
|
28
|
+
} finally {
|
|
29
|
+
try {await docker(['down']);}finally{await fs.rm(root,{recursive:true,force:true});}
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Real Docker replacement using WhatsApp's synthetic transport. No provider calls.
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {execFile} from 'node:child_process';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import {snapshot,init,atomic} from '../src/plugins/manager.mjs';
|
|
9
|
+
import {bindUpdates} from '../src/updates/binding.mjs';
|
|
10
|
+
import {prepare,submit,jobPath,read} from '../src/updates/control.mjs';
|
|
11
|
+
import {perform} from '../src/updates/runtime.mjs';
|
|
12
|
+
import {version} from '../src/updates/artifact.mjs';
|
|
13
|
+
const exec=promisify(execFile),root=await fs.realpath(await fs.mkdtemp(path.join(tmpdir(),'ez-upgrade-docker-')));
|
|
14
|
+
const source=path.join(root,'source'),home=path.join(root,'tools'),mind=path.join(root,'mind'),control=path.join(root,'control');
|
|
15
|
+
const input=path.resolve(process.env.EZ_WHATSAPP_SOURCE||new URL('../../ez_whatsapp',import.meta.url).pathname);
|
|
16
|
+
const bin=new URL('../bin/ezenciel-agents-tools.mjs',import.meta.url).pathname;
|
|
17
|
+
const call=async(...args)=>JSON.parse((await exec(process.execPath,[bin,'--home',home,...args],{maxBuffer:4*1024*1024})).stdout);
|
|
18
|
+
let record;
|
|
19
|
+
try {
|
|
20
|
+
for(const dir of [source,mind,control])await fs.mkdir(dir);
|
|
21
|
+
const original=await snapshot(input);
|
|
22
|
+
const [major,minor,patch]=version(original.manifest.version).numbers;
|
|
23
|
+
const upgradedVersion=`${major}.${minor}.${patch+1}-qa.1`,brokenVersion=`${major}.${minor}.${patch+2}-qa.1`;
|
|
24
|
+
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});}
|
|
25
|
+
await fs.copyFile(path.join(input,'docker/fixture.mjs'),path.join(source,'src/transport.mjs'));
|
|
26
|
+
await init(home,mind);await atomic(path.join(root,'host-executor.json'),{cli:'grok',agents:[{name:'qa',workspace:mind,controlDir:control,toolsHome:home,binDir:path.join(home,'bin')}]});
|
|
27
|
+
await bindUpdates(home,path.join(root,'host-executor.json'));
|
|
28
|
+
const initial=await snapshot(source);await call('plugins','install','whatsapp','--source',source,'--revision',initial.revision);
|
|
29
|
+
record=(await read(path.join(home,'registry.json'))).plugins.whatsapp;
|
|
30
|
+
await call('plugins','start','whatsapp');
|
|
31
|
+
const identity=await call('whatsapp','doctor');const before=await call('whatsapp','inbox');
|
|
32
|
+
const message=path.join(mind,'test-message.txt');await fs.writeFile(message,'Synthetic upgrade fixture');
|
|
33
|
+
const sent=await call('whatsapp','send','--to','+15551230000','--text-file',message,'--idempotency-key','upgrade:fixture');
|
|
34
|
+
const build=async(version,broken=false)=>{
|
|
35
|
+
const pkg=await read(path.join(source,'package.json'));pkg.version=version;await atomic(path.join(source,'package.json'),pkg);
|
|
36
|
+
const manifest=await read(path.join(source,'ez-plugin.json'));manifest.version=version;await atomic(path.join(source,'ez-plugin.json'),manifest);
|
|
37
|
+
if(broken)await fs.writeFile(path.join(source,'src/transport.mjs'),'throw Error("Synthetic broken candidate")');
|
|
38
|
+
const [pack]=JSON.parse((await exec('npm',['pack','--ignore-scripts','--json','--pack-destination',root],{cwd:source})).stdout);
|
|
39
|
+
const job=await prepare(home,'whatsapp',{file:path.join(root,pack.filename)});
|
|
40
|
+
await atomic(path.join(home,'updates/supervisor.json'),{at:Date.now()});await submit(home,job.id,false);
|
|
41
|
+
return perform(home,await read(path.join(jobPath(home,job.id),'job.json')),{startHost:()=>{throw Error('Plugin touched host');},stopHost:()=>{throw Error('Plugin touched host');}});
|
|
42
|
+
};
|
|
43
|
+
assert.equal((await build(upgradedVersion)).status,'completed');
|
|
44
|
+
assert.equal((await call('whatsapp','doctor')).data.connected,identity.data.connected);
|
|
45
|
+
assert.equal((await call('whatsapp','inbox')).data.nextCursor,before.data.nextCursor);
|
|
46
|
+
assert.equal((await call('whatsapp','operation','--idempotency-key','upgrade:fixture')).data.providerMessageId,sent.data.providerMessageId);
|
|
47
|
+
assert.equal((await build(brokenVersion,true)).status,'rolled-back');
|
|
48
|
+
assert.equal((await call('whatsapp','doctor')).data.connected,true);
|
|
49
|
+
assert.equal((await call('whatsapp','operation','--idempotency-key','upgrade:fixture')).data.providerMessageId,sent.data.providerMessageId);
|
|
50
|
+
const current=(await read(path.join(home,'registry.json'))).plugins.whatsapp;
|
|
51
|
+
assert.equal(current.project,record.project);assert.equal(current.manifest.version,upgradedVersion);
|
|
52
|
+
console.log('PASS: Docker plugin upgrade, private volume backup, retained identity/cursor/operation receipt, failed-health rollback. Synthetic provider only.');
|
|
53
|
+
} finally {
|
|
54
|
+
if(record)await exec('docker',['compose','-p',record.project,'-f',record.compose,'down','--volumes']).catch(()=>{});
|
|
55
|
+
// Delete only fixture-owned images and state.
|
|
56
|
+
if(record){const images=(await exec('docker',['image','ls','--filter',`reference=${record.project}-*`,'-q'])).stdout.trim().split('\n').filter(Boolean);if(images.length)await exec('docker',['image','rm',...images]).catch(()=>{});}
|
|
57
|
+
await fs.rm(root,{recursive:true,force:true});
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# AI selection
|
|
2
|
+
|
|
3
|
+
`menu.ts` owns Telegram controls; `ai.ts` projects installed-client metadata.
|
|
4
|
+
`ControlStore` stores saved presets, the next-conversation default and current choice.
|
|
5
|
+
No provider SDK, model fallback, history migration, or second execution loop.
|
|
6
|
+
|
|
7
|
+
At intake each journal entry receives an immutable preset and conversation ID.
|
|
8
|
+
Batches cannot cross that boundary. The run copies that choice and invokes the
|
|
9
|
+
native adapter with its exact model/effort. A later menu change cannot reroute it.
|
|
10
|
+
Old conversation metadata is retained only so accepted work can finish; selecting
|
|
11
|
+
a different CLI never restores that CLI's old history.
|
|
12
|
+
|
|
13
|
+
Grok/Claude accept caller-selected native UUIDs. Codex/OpenCode generate IDs, so
|
|
14
|
+
the adapter reads only their typed JSONL session metadata; stdout never becomes a
|
|
15
|
+
Telegram reply. `codex-gui` is a separate desktop adapter: it submits a dedicated
|
|
16
|
+
app-server thread/turn and waits for that turn to finish. It does not run
|
|
17
|
+
`codex exec`. A missing or mismatched native session fails closed, not `--last`.
|
|
18
|
+
Legacy unbound sessions require the owner's explicit `/new`.
|
|
19
|
+
|
|
20
|
+
References used for the adapter contract:
|
|
21
|
+
|
|
22
|
+
- [Codex non-interactive execution](https://learn.chatgpt.com/docs/non-interactive-mode)
|
|
23
|
+
- [OpenCode native run events](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts)
|
|
24
|
+
|
|
25
|
+
The catalog is local metadata, not an authentication or billing health check.
|
|
26
|
+
Grok/Codex support explicit listed model/effort choices. Other installed clients
|
|
27
|
+
offer their own default only in this slice. Refresh by opening the native client;
|
|
28
|
+
the relay does not install models, manage subscriptions or guess aliases.
|
|
29
|
+
|
|
30
|
+
Setup initialization and relay startup seed one default choice per installed client.
|
|
31
|
+
Settings → Refresh available AIs repeats discovery. Active/default presets and
|
|
32
|
+
queued snapshots are preserved; discovery only refreshes unused detected entries.
|
|
33
|
+
Codex uses its native `config/read` interface; Grok reads its documented user
|
|
34
|
+
model/effort settings (or `models` for the default model). Claude reads user and
|
|
35
|
+
workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
|
|
36
|
+
opaque wrappers remain explicitly “client default”. No credentials are stored,
|
|
37
|
+
no inference runs, no new dependency, and no cross-CLI session transfer.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Authority boundaries
|
|
2
|
+
|
|
3
|
+
The relay pairs one verified Telegram owner before execution; foreign senders
|
|
4
|
+
and groups do not acquire execution rights. Replies use the run-bound source
|
|
5
|
+
chat. Plugin events are untrusted content, queued in the same single-writer lane;
|
|
6
|
+
subscription permission is not permission to reply or execute incoming demands.
|
|
7
|
+
|
|
8
|
+
The host CLI runs as the trusted installing user. Its Markdown role, confirmation
|
|
9
|
+
tools, environment filtering and private state layout do not create adversarial
|
|
10
|
+
OS isolation. The plugin manager has Docker administration access. Container
|
|
11
|
+
profile separation does not protect against a hostile host administrator.
|
|
12
|
+
|
|
13
|
+
See [SECURITY.md](../../SECURITY.md) for the supported security scope and
|
|
14
|
+
[Docker runtime](../docker-runtime.md) for the actual process/storage boundary.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Local event sources
|
|
2
|
+
|
|
3
|
+
A plugin owns capture, authentication and eligibility. The relay owns execution.
|
|
4
|
+
Register through `ezenciel-agents-source --name NAME --socket /absolute/service.sock`;
|
|
5
|
+
inspect with `--list`, remove with `--name NAME --remove`. Registration pins the
|
|
6
|
+
paired owner, assigns a new binding ID and starts at the provider's current head.
|
|
7
|
+
Same-user plugins are trusted installed code; private Unix sockets are the boundary,
|
|
8
|
+
not a sandbox against other programs running as that user.
|
|
9
|
+
|
|
10
|
+
POST JSON `{ "command": "...", "args": {} }` to `/` over the Unix socket.
|
|
11
|
+
Return HTTP 200 with `{ "ok": true, "data": ... }`:
|
|
12
|
+
|
|
13
|
+
- `events-head`: `{cursor: <nonnegative integer>}`.
|
|
14
|
+
- `events`, args `{after: <cursor>}`: `{cursor, events}`. Advance over excluded
|
|
15
|
+
events too. IDs must remain stable; return at most ten events in capture order.
|
|
16
|
+
- `events-check`, args `{ids: [<string IDs>]}`: `{events}` containing only those
|
|
17
|
+
IDs still eligible now, with current canonical content.
|
|
18
|
+
|
|
19
|
+
Each event is `{id, conversationId, receivedAt, text}`. IDs are at most 100 ASCII
|
|
20
|
+
letters/digits/underscore/hyphen; conversation IDs at most 200 characters;
|
|
21
|
+
receivedAt is epoch milliseconds; text at most 16000 characters. Responses are
|
|
22
|
+
bounded to 256 KiB and three seconds. Plugin-specific policy stays in the plugin.
|
|
23
|
+
|
|
24
|
+
The host polls each second and waits for two seconds of quiet, ten seconds of
|
|
25
|
+
age, or ten events. It groups by conversation and persists the batch before
|
|
26
|
+
creating deterministic run IDs. Cursor acknowledgement follows durable run
|
|
27
|
+
creation. Crash replay reuses the batch and run IDs. This deduplicates queue
|
|
28
|
+
creation; it does not promise exactly-once external actions after executor failure.
|
|
29
|
+
|
|
30
|
+
Before starting queued work, recheck binding, owner and provider eligibility.
|
|
31
|
+
Unavailable sources keep work queued; removed subscriptions cancel empty runs.
|
|
32
|
+
No check can retract work already started. External observations use fresh executor
|
|
33
|
+
sessions and explicitly carry no owner-instruction or send authority. They share
|
|
34
|
+
the existing one-writer queue and secret whitelist. No provider SDK is imported.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Telegram intake and recovery
|
|
2
|
+
|
|
3
|
+
One owner, one bot, one relay writer per control directory. No database, extra agent, or executor loop.
|
|
4
|
+
|
|
5
|
+
1. Gate the sender and private chat before accepting work.
|
|
6
|
+
2. Atomically save the raw update and deduplicate its Telegram update ID in `EZ_CONTROL_DIR/inbox.json`.
|
|
7
|
+
3. Collect a short burst before downloading/transcribing. Seal batch membership on disk, then normalize in receive order. Captions, album IDs and quoted context travel with the media.
|
|
8
|
+
4. Create one durable run using the batch's stable ID. A restart between run creation and intake completion finds that same run; it does not create a second job.
|
|
9
|
+
5. The selected CLI executes. Replies still come through the messaging CLI and the receipt-backed outbox.
|
|
10
|
+
|
|
11
|
+
The polling handler does not wait for media processing or execution. Native controls bypass the work queue, but not the owner gate. Buffered work is checked against the current owner again before processing and execution.
|
|
12
|
+
|
|
13
|
+
## Recovery boundaries
|
|
14
|
+
|
|
15
|
+
- Process restart recovers accepted, unstarted messages. Stopping the relay does not discard its intake buffer.
|
|
16
|
+
- Attachment downloads and transcription retry transient network errors, timeouts, HTTP 408/429 and selected 5xx responses up to three total attempts. Backoff starts at one second, then two; Retry-After is honored up to thirty seconds, with longer cooldowns left for explicit recovery. Each request has a sixty-second timeout, including body reads. Authentication, certificate and invalid-input errors are not automatically retried. Transcription retries can incur additional provider charges; there is no provider/model fallback.
|
|
17
|
+
- Exhausted or permanent normalization failures quarantine the whole batch; later batches can proceed. `/status` exposes the failure and owner-only `/retry` requeues the latest failed incoming batch with its original stable run ID and context. No incomplete-context execution, replay of executed jobs, or automatic retry of external actions/chat sends. Errors identify the request stage and network code without logging credential-bearing URLs or provider payloads.
|
|
18
|
+
- `/cancel` cancels buffered/normalizing intake and queued runs, not active execution. A download already in progress may finish staging a file, but its cancelled batch cannot launch.
|
|
19
|
+
- `/stop` requests termination of active execution only. It neither clears the queue nor promises a successful same-session resume in the CLI.
|
|
20
|
+
- Existing running records are not blindly replayed. Unknown execution/delivery outcomes require local inspection; this is not an exactly-once guarantee for external side effects.
|
|
21
|
+
- `/new` preserves files and queued work. It is not a cancellation command.
|
|
22
|
+
- Approval decisions are replay-idempotent only for the same authenticated Telegram update. Another click cannot replay consent. Buttons record consent; they do not constrain a full-access executor's tools.
|
|
23
|
+
- A broken intake journal fails closed, preserving the file. Polling must not acknowledge work it could not persist. Repair it locally before restarting.
|
|
24
|
+
|
|
25
|
+
## Deliberate limits
|
|
26
|
+
|
|
27
|
+
The quiet window is two seconds, bounded by thirty seconds/ten messages. Received album items remain together, including at the cap; arbitrarily late attachments cannot be guaranteed to join an earlier turn.
|
|
28
|
+
|
|
29
|
+
The local JSON journal retains update-ID deduplication tombstones and failed/cancelled batches. It is not a high-volume multi-tenant queue; retention/compaction and automatic service startup belong to later installation work. Use separate control directories for separate bots. Machine-level isolation and disk power-loss durability are not claimed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Development and testing
|
|
2
|
+
|
|
3
|
+
Use Node 22+ and pnpm 10.30.3: `pnpm install --frozen-lockfile`, then
|
|
4
|
+
`pnpm verify`. Tests use temporary synthetic state; TypeScript checks source and
|
|
5
|
+
tests. Use `npm run release:check` for package contents. See CONTRIBUTING.md for
|
|
6
|
+
PR expectations and [release checks](releasing.md) for Docker and clean-host QA.
|
|
7
|
+
|
|
8
|
+
For the cross-repository fixture, place the reviewed WhatsApp source beside this
|
|
9
|
+
repository as `ez_whatsapp` and run `node docker/plugin-smoke.mjs`. For another
|
|
10
|
+
location, set `EZ_WHATSAPP_SOURCE=/absolute/reviewed/whatsapp/package`. It installs
|
|
11
|
+
through the real manager with synthetic provider data, checks dispatch,
|
|
12
|
+
operation replay, restart and retained data on uninstall, then removes its own
|
|
13
|
+
fixture resources. It never uses an existing linked account.
|
|
14
|
+
|
|
15
|
+
`pnpm smoke` contacts Telegram and invokes the selected AI. It proves outbound
|
|
16
|
+
reply delivery, not incoming onboarding. Stop the exact poller first, use a
|
|
17
|
+
dedicated authorized account, and follow docs/setup.md for the full incoming
|
|
18
|
+
path. Never revoke a working owner or reuse production profiles as fixtures.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Docker runtime and shared host CLI
|
|
2
|
+
|
|
3
|
+
Docker Compose owns each relay and executable plugin. The CLI the user installs
|
|
4
|
+
Ez from stays on the host, with its existing authentication. All agents in that
|
|
5
|
+
installation use that CLI; there is no second CLI installation or agent-specific
|
|
6
|
+
CLI login. The relay image contains Node, relay dependencies and ffmpeg.
|
|
7
|
+
|
|
8
|
+
## Agent binding
|
|
9
|
+
|
|
10
|
+
Use [agent-led setup](setup.md). `ezenciel-agents-create` accepts a name, purpose,
|
|
11
|
+
a bot token through stdin; the CLI defaults to the one recorded at package
|
|
12
|
+
installation by `--register-cli <current-cli>`. It creates a private deployment
|
|
13
|
+
with a unique Compose project, mind, control directory and secret file. Names
|
|
14
|
+
cannot overwrite existing agents. `installation.json` keeps subsequent agents
|
|
15
|
+
on the same CLI. Each agent gets its own purpose and native conversation IDs.
|
|
16
|
+
|
|
17
|
+
`docker.env` contains explicit absolute paths, never token values:
|
|
18
|
+
|
|
19
|
+
```dotenv
|
|
20
|
+
COMPOSE_PROJECT_NAME=ez-agent-family
|
|
21
|
+
EZ_EXECUTOR_CLI=grok
|
|
22
|
+
EZ_AGENT_WORKSPACE=/absolute/private/agents/family/mind
|
|
23
|
+
EZ_CONTROL_DIR=/absolute/private/agents/family/control
|
|
24
|
+
EZ_RELAY_ENV_FILE=/absolute/private/agents/family/relay.env
|
|
25
|
+
EZ_AGENT_PURPOSE_FILE=/absolute/private/agents/family/purpose.md
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The installing agent runs `ezenciel-agents-host` under the host's service manager
|
|
29
|
+
with `EZ_DEPLOYMENT_DIR` bound to that deployment. This small transport invokes
|
|
30
|
+
the existing CLI; it is not a second relay or model loop. It reads requests from
|
|
31
|
+
the agent's control directory, fixes cwd/control/tool paths from its installation
|
|
32
|
+
binding, strips environment secrets, forwards native output and exit status, and
|
|
33
|
+
propagates cancellation. Requests cannot select another executable. No network
|
|
34
|
+
listener, Docker socket in containers, new provider API or per-CLI service shim
|
|
35
|
+
is required. Host Node 22+ and the package dependencies run this transport.
|
|
36
|
+
|
|
37
|
+
Mount mind and control at identical absolute paths in Docker and on the host,
|
|
38
|
+
so incoming files, message attachments and CLI outputs need no path translation.
|
|
39
|
+
Only that agent's directories are mounted into its relay. The host CLI reuses the existing login. Codex receives a private per-agent
|
|
40
|
+
state home with linked authentication; global memory/configuration is excluded.
|
|
41
|
+
This is not an OS security boundary between agents running as the same user.
|
|
42
|
+
Plugin setup also binds `toolsHome` in the host configuration. Codex receives
|
|
43
|
+
write access to that registry and network access for the Docker client; the host
|
|
44
|
+
rejects registries belonging to a different workspace. Restart the host worker
|
|
45
|
+
between jobs after adding a registry binding.
|
|
46
|
+
|
|
47
|
+
## Operate and verify
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
export EZ_DEPLOYMENT_DIR=/absolute/private/agents/family
|
|
51
|
+
bin/ezenciel-agents-docker up -d --wait
|
|
52
|
+
bin/ezenciel-agents-docker run --rm relay owner status
|
|
53
|
+
bin/ezenciel-agents-docker run --rm relay owner approve <verified-numeric-id>
|
|
54
|
+
bin/ezenciel-agents-docker stop relay
|
|
55
|
+
bin/ezenciel-agents-docker run --rm relay smoke
|
|
56
|
+
bin/ezenciel-agents-docker up -d --wait
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The owner supplies a real DM; verify the pending numeric sender before pairing.
|
|
60
|
+
Live smoke uses the same host CLI and requires an actual Telegram receipt.
|
|
61
|
+
Restart preserves pairing and files. One kernel lock excludes relay/smoke
|
|
62
|
+
writers; exit 73 means a writer is active. Do not delete its lock to bypass it.
|
|
63
|
+
Health requires recent polling and host-transport heartbeats, not just a process.
|
|
64
|
+
Model catalog metadata is exported from the selected host CLI without credentials.
|
|
65
|
+
The AI menu stays within that CLI. No automatic executor fallback is performed.
|
|
66
|
+
|
|
67
|
+
## Secrets and plugins
|
|
68
|
+
|
|
69
|
+
The relay secret file is private and outside the mind. A short root bootstrap
|
|
70
|
+
opens it, protects `/run/secrets`, and drops capabilities. The relay runs with
|
|
71
|
+
distinct real/effective UIDs to deny process-memory reads; local container tool
|
|
72
|
+
processes normalize to UID 1000. Relay secrets never enter configured container
|
|
73
|
+
environment or host CLI environment. Container isolation tests cover these
|
|
74
|
+
boundaries; the trusted host account can still administer installation files.
|
|
75
|
+
|
|
76
|
+
Each plugin owns its Docker image, dependencies, private profile, onboarding
|
|
77
|
+
and receipts. The agent-bound `ez` registry is the only plugin authority:
|
|
78
|
+
`ez plugins list`, `ez plugins install`, `ez plugins start|stop|status`, and
|
|
79
|
+
`ez <alias> ...`. Its reviewed deployment descriptor supplies literal command
|
|
80
|
+
and volume bindings. Never add standalone provider launchers or deployments.
|
|
81
|
+
Read the installed skill from the registry before onboarding; do not re-pair
|
|
82
|
+
an already connected account. Monitoring and sends require their own authority.
|
|
83
|
+
|
|
84
|
+
For event intake, mount only the registered plugin project's socket/client
|
|
85
|
+
exports into the relay, read-only. The event source stores a cursor and policy
|
|
86
|
+
binding; it is not a second plugin installation. Provider credentials remain in
|
|
87
|
+
the plugin profile. Keep this event mount aligned with the registry-owned project.
|
|
88
|
+
|
|
89
|
+
## Migration and rollback
|
|
90
|
+
|
|
91
|
+
Stop and disable the exact old poller/profile service. Back up mind, control,
|
|
92
|
+
plugin profile and relevant native sessions first. Copy canonical relay state to
|
|
93
|
+
its per-agent bind directories and plugin state into its own volume. Preserve
|
|
94
|
+
pending/uncertain operations without replay. Never run two copies of a bot token
|
|
95
|
+
or linked profile. Keep the same selected host CLI and its existing login.
|
|
96
|
+
|
|
97
|
+
To roll back, stop Docker and the host transport first, deliberately reconcile
|
|
98
|
+
newer state, then restore the recorded old service. Never use `down -v` on a live
|
|
99
|
+
profile. Docker smoke uses fixtures; it is not proof of provider delivery, phone
|
|
100
|
+
UI, voice or buttons. Run the explicit real smoke separately while the poller is
|
|
101
|
+
stopped. A previously containerized native session needs deliberate history/path
|
|
102
|
+
migration before `--resume`; do not silently reset it.
|
|
103
|
+
|
|
104
|
+
The installing CLI is an initial default, not a permanent restriction. An explicit
|
|
105
|
+
owner selection may use any supported CLI/model installed on the host. Native
|
|
106
|
+
`ezenciel-agents-ai list` and `select` expose this choice. A cross-CLI switch
|
|
107
|
+
starts a fresh native conversation and preserves the mind and installation
|
|
108
|
+
default. Existing queued jobs retain their captured execution choice.
|
|
109
|
+
|
|
110
|
+
## Codex context isolation
|
|
111
|
+
|
|
112
|
+
The host Codex binary and existing login are reused, but each agent has its own
|
|
113
|
+
`control/cli/codex` state directory. Only authentication is linked to the host
|
|
114
|
+
login; global configuration, sessions and memories are not imported. Global
|
|
115
|
+
memory and host skill discovery are disabled for relay jobs. A conversation
|
|
116
|
+
that already received unrelated global context must be replaced with a fresh
|
|
117
|
+
native conversation; disabling injection does not remove prior turn content.
|
|
118
|
+
This prevents automatic context sharing, not adversarial access by the host user.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Host transport startup
|
|
2
|
+
|
|
3
|
+
Register one service per deployment after initializing its tools. Substitute the
|
|
4
|
+
exact absolute paths below. Use the existing authenticated host user and include
|
|
5
|
+
its Node 22+, pnpm (or Corepack), Docker and CLI directories in PATH. Resolve the
|
|
6
|
+
actual installed launchers first; shell aliases and interactive shell startup
|
|
7
|
+
files are not available to services. No bot token goes in these service files.
|
|
8
|
+
Docker Compose owns the relay/plugins; this service only runs the host transport.
|
|
9
|
+
`ezenciel-agents-setup service` starts only the Docker relay; it does not register
|
|
10
|
+
or start this host service.
|
|
11
|
+
|
|
12
|
+
## Linux
|
|
13
|
+
|
|
14
|
+
Save `~/.config/systemd/user/ez-family.service` with a unique agent name:
|
|
15
|
+
|
|
16
|
+
```ini
|
|
17
|
+
[Unit]
|
|
18
|
+
Description=Ez family host CLI transport
|
|
19
|
+
|
|
20
|
+
[Service]
|
|
21
|
+
Type=simple
|
|
22
|
+
Environment=EZ_DEPLOYMENT_DIR=/absolute/private/agents/family
|
|
23
|
+
Environment=PATH=/absolute/node/bin:/absolute/package-manager/bin:/absolute/cli/bin:/usr/local/bin:/usr/bin:/bin
|
|
24
|
+
ExecStart=/absolute/ez-package/package/bin/ezenciel-agents-host
|
|
25
|
+
Restart=on-failure
|
|
26
|
+
RestartSec=5
|
|
27
|
+
|
|
28
|
+
[Install]
|
|
29
|
+
WantedBy=default.target
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
systemctl --user daemon-reload
|
|
34
|
+
systemctl --user enable --now ez-family.service
|
|
35
|
+
systemctl --user status ez-family.service
|
|
36
|
+
journalctl --user -u ez-family.service -n 50 --no-pager
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Startup before login additionally needs administrator-enabled user lingering
|
|
40
|
+
(`loginctl enable-linger <host-user>`); verify `loginctl show-user <host-user> -p
|
|
41
|
+
Linger`. Docker must start at boot too. Verify the real CLI tool call under this
|
|
42
|
+
service environment. Stop/disable only this unit when removing this agent.
|
|
43
|
+
|
|
44
|
+
## macOS
|
|
45
|
+
|
|
46
|
+
Save `~/Library/LaunchAgents/local.ez.family.plist` with a unique agent label:
|
|
47
|
+
|
|
48
|
+
```xml
|
|
49
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<plist version="1.0"><dict>
|
|
51
|
+
<key>Label</key><string>local.ez.family</string>
|
|
52
|
+
<key>ProgramArguments</key><array><string>/absolute/ez-package/package/bin/ezenciel-agents-host</string></array>
|
|
53
|
+
<key>EnvironmentVariables</key><dict>
|
|
54
|
+
<key>EZ_DEPLOYMENT_DIR</key><string>/absolute/private/agents/family</string>
|
|
55
|
+
<key>PATH</key><string>/absolute/node/bin:/absolute/package-manager/bin:/absolute/cli/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
56
|
+
</dict>
|
|
57
|
+
<key>RunAtLoad</key><true/>
|
|
58
|
+
<key>KeepAlive</key><true/>
|
|
59
|
+
</dict></plist>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
plutil -lint "$HOME/Library/LaunchAgents/local.ez.family.plist"
|
|
64
|
+
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/local.ez.family.plist"
|
|
65
|
+
launchctl print "gui/$(id -u)/local.ez.family"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
LaunchAgents start at login, not before login. Configure Docker's normal login
|
|
69
|
+
startup and verify after reboot/login. Use `launchctl bootout` with the same
|
|
70
|
+
domain/file before replacing/removing this service. Never start a duplicate
|
|
71
|
+
transport for a deployment. A service listing is insufficient: check heartbeat,
|
|
72
|
+
Compose health and an actual Telegram reply.
|
|
73
|
+
|
|
74
|
+
Linux and macOS are documented host paths. Windows and GUI executor acceptance
|
|
75
|
+
are not certified by these instructions or the headless Docker tests.
|
|
76
|
+
|
|
77
|
+
## Agent-owned upgrades
|
|
78
|
+
|
|
79
|
+
This beta includes owner-policy release checks and durable
|
|
80
|
+
main/plugin replacement. See [upgrade setup, tools and recovery](upgrades.md). Earlier main upgrade/rollback VM QA passed; final-release fresh-host/reboot and live plugin upgrade acceptance remain pending.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Adding a plugin
|
|
2
|
+
|
|
3
|
+
A plugin is an independent repository, CLI and container with a skill teaching
|
|
4
|
+
its use. Keep provider code out of the relay. Reuse the main package's documented
|
|
5
|
+
plugin/deployment schema; do not introduce a framework or provider-specific router.
|
|
6
|
+
|
|
7
|
+
Before the first PR, provide:
|
|
8
|
+
|
|
9
|
+
- README with purpose, requirements, exact install/start/onboarding commands,
|
|
10
|
+
working read example, verified identity, limitations and troubleshooting.
|
|
11
|
+
- package.json, frozen lockfile, LICENSE and third-party notices; reviewed npm
|
|
12
|
+
files allowlist, Dockerfile and .dockerignore; matching manifest/package version.
|
|
13
|
+
- ez-plugin.json, ez-deployment.json and a skill covering discovery, required
|
|
14
|
+
inputs, private credentials, QR/OAuth handoff, resumption and readiness proof.
|
|
15
|
+
- `--help`, read-only doctor, explicit account binding, bounded reads and stable
|
|
16
|
+
machine output/exit codes. For writes: operation key, readback and uncertainty
|
|
17
|
+
handling; no blind retry. Provider content cannot grant execution authority.
|
|
18
|
+
- Private state locations, start/stop/status, backup, migration/rollback limits,
|
|
19
|
+
data-preserving uninstall and separate account revocation instructions.
|
|
20
|
+
- Offline contract/negative tests and CI. Verify snapshot installation and CLI
|
|
21
|
+
dispatch through the actual Ez manager, restart persistence and removal using
|
|
22
|
+
synthetic data, then an authorized provider operation from the real executor.
|
|
23
|
+
|
|
24
|
+
Copy CONTRIBUTING.md, SECURITY.md structure, PR template and docs/releasing.md
|
|
25
|
+
from a released Ez repository; adapt commands and security facts to your plugin.
|
|
26
|
+
Do not copy private QA, company policy, proposed features or unsupported claims.
|
|
27
|
+
Public instructions must work without the maintainer's parent workspace. Use
|
|
28
|
+
reviewed local source and its inspected content hash; registry installation is
|
|
29
|
+
inert. Installation is complete only after account onboarding and verified use.
|
|
30
|
+
|
|
31
|
+
## Agent-owned upgrades
|
|
32
|
+
|
|
33
|
+
This beta includes owner-policy release checks and durable
|
|
34
|
+
main/plugin replacement. See [upgrade setup, tools and recovery](upgrades.md). Earlier main upgrade/rollback VM QA passed; final-release fresh-host/reboot and live plugin upgrade acceptance remain pending.
|