@jc_stack/ez-agents 0.1.0-beta.42 → 0.1.0-beta.44
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/CHANGELOG.md +24 -0
- package/Dockerfile +3 -1
- package/bin/ez +3 -1
- package/compose.yaml +2 -0
- package/docker/entrypoint.sh +4 -1
- package/docker/smoke.mjs +92 -1
- package/docs/application-channel.md +6 -2
- package/docs/architecture/ai-selection.md +7 -4
- package/docs/architecture/authority-boundaries.md +15 -4
- package/docs/docker-runtime.md +25 -2
- package/docs/local-qa.md +5 -0
- package/docs/plugin-guide.md +10 -2
- package/docs/plugins.md +22 -2
- package/docs/selective-monitoring.md +26 -12
- package/docs/upgrades.md +5 -1
- package/package.json +1 -1
- package/src/agent-install.ts +5 -3
- package/src/ai-cli.ts +13 -4
- package/src/ai.ts +7 -3
- package/src/audio.ts +4 -2
- package/src/client-defaults.ts +7 -4
- package/src/control-state.ts +12 -8
- package/src/conversation-menu.ts +5 -2
- package/src/executor.ts +5 -5
- package/src/menu.ts +9 -7
- package/src/plugin-broker.mjs +2 -0
- package/src/plugins/manager.mjs +43 -9
- package/src/plugins/native-tasks.d.mts +1 -0
- package/src/plugins/native-tasks.mjs +13 -0
- package/src/task-cli.ts +1 -1
- package/src/task-executor.ts +7 -4
- package/src/tasks.ts +54 -16
- package/src/updates/control.mjs +5 -3
- package/src/updates/runtime.mjs +27 -7
- package/test/agent-install.test.ts +6 -0
- package/test/ai-cli.test.ts +6 -1
- package/test/ai.test.ts +69 -14
- package/test/application-controls.test.ts +2 -1
- package/test/conversation-menu.test.ts +2 -0
- package/test/conversations.test.ts +26 -0
- package/test/plugin-broker.test.mjs +13 -1
- package/test/plugin-manager.test.mjs +58 -1
- package/test/scheduler-host.test.ts +1 -0
- package/test/task-native.test.ts +2 -4
- package/test/tasks.test.ts +107 -3
- package/test/updates.test.mjs +55 -3
- package/test/upgrade-pause.test.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.44
|
|
4
|
+
|
|
5
|
+
- Let isolated native agents read declared installed plugin skills through the
|
|
6
|
+
existing authorized broker. Discovery supplies literal skill-read commands,
|
|
7
|
+
so tools such as EzStudy work without copying package instructions into the
|
|
8
|
+
agent workspace or rebuilding native context.
|
|
9
|
+
- Keep skill reads bound to the installed revision and active authorized run;
|
|
10
|
+
reject undeclared paths, traversal, symlinks and oversized files.
|
|
11
|
+
|
|
12
|
+
## 0.1.0-beta.43
|
|
13
|
+
|
|
14
|
+
- Keep model and reasoning settings with each named conversation and preserve
|
|
15
|
+
native history across supported model changes. Save the selected AI as the
|
|
16
|
+
default for new conversations.
|
|
17
|
+
- Keep private application-channel AI choices separate from owner preset slots.
|
|
18
|
+
- Honor current-owner-authorized messaging tasks without a second approval;
|
|
19
|
+
retain bounded task authority across restart without replaying uncertain sends.
|
|
20
|
+
- Expose installed native controls through Ez help and preserve operator CLI pins,
|
|
21
|
+
isolated credentials, plugin volumes and private broker ownership on Linux.
|
|
22
|
+
- Reuse the exact healthy pinned relay during failed-update recovery.
|
|
23
|
+
- Legacy deployments need the reviewed installer migration in
|
|
24
|
+
docs/application-channel.md for the trusted broker capability change; ordinary
|
|
25
|
+
updater compatibility guards remain in force.
|
|
26
|
+
|
|
3
27
|
## 0.1.0-beta.42
|
|
4
28
|
|
|
5
29
|
- Keep recurring schedule edits at their next pending or future occurrence, so
|
package/Dockerfile
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
FROM docker/compose-bin:v5.5.1@sha256:14162382692aae43977f79effa64a499d61de19d4645461881daaf88773f7e74 AS compose-bin
|
|
1
2
|
FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca6d3ea96f16cb30df6187d94 AS dependencies
|
|
2
3
|
WORKDIR /app
|
|
3
|
-
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg docker.io
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg docker.io && rm -rf /var/lib/apt/lists/*
|
|
5
|
+
COPY --from=compose-bin /docker-compose /usr/local/bin/docker-compose
|
|
4
6
|
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
|
|
5
7
|
COPY package.json ./
|
|
6
8
|
COPY docker/pnpm-lock.yaml ./pnpm-lock.yaml
|
package/bin/ez
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createConnection } from 'node:net';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { installedNativeHelp } from '../src/plugins/native-tasks.mjs';
|
|
4
6
|
|
|
5
7
|
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
6
8
|
const RUN_ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
@@ -86,7 +88,7 @@ const manager = async args => {
|
|
|
86
88
|
const main = async () => {
|
|
87
89
|
const args = process.argv.slice(2);
|
|
88
90
|
if (!args.length || args[0] === '--help' || args[0] === '-h') {
|
|
89
|
-
process.stdout.write('
|
|
91
|
+
process.stdout.write(JSON.stringify({commands:['tools list [--details]','tools skill <plugin> <declared-path>','<registered CLI> ...'],native:await installedNativeHelp(fileURLToPath(new URL('.',import.meta.url)))})+'\n');
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
const stdin = ['plugins', 'tools', 'updates', 'status'].includes(args[0]) ? '' : await readStdin();
|
package/compose.yaml
CHANGED
package/docker/entrypoint.sh
CHANGED
|
@@ -21,7 +21,10 @@ if [ "$(id -u)" = 0 ]; then
|
|
|
21
21
|
chmod 700 /run/secrets
|
|
22
22
|
chown "$runtime_uid:$runtime_gid" /state/control /state/home /workspace
|
|
23
23
|
if [ -f /run/secrets/relay_env ]; then exec 3</run/secrets/relay_env; else exec 3</dev/null; fi
|
|
24
|
-
|
|
24
|
+
# Some Linux security modules normalize differing real/effective UIDs during
|
|
25
|
+
# an exec under no_new_privs. Preserve the relay's split identity here; the
|
|
26
|
+
# executor applies no_new_privs after normalizing its own IDs.
|
|
27
|
+
exec setpriv --ruid="$relay_uid" --euid="$runtime_uid" --regid="$runtime_gid" --clear-groups --bounding-set=-all /app/docker/entrypoint.sh "$@"
|
|
25
28
|
fi
|
|
26
29
|
# Reserve the private environment descriptor before Node can use it for libuv.
|
|
27
30
|
# Root startup already opened it; preserve that inherited secret descriptor.
|
package/docker/smoke.mjs
CHANGED
|
@@ -8,6 +8,10 @@ const dir = mkdtempSync(join(tmpdir(), 'ez-docker-qa-'));
|
|
|
8
8
|
const holder = `ez-main-lock-qa-${process.pid}`;
|
|
9
9
|
const volume = `${holder}-control`;
|
|
10
10
|
const application = `${holder}-application`;
|
|
11
|
+
const privateVolume = `${holder}-private`;
|
|
12
|
+
const boundProject = `${holder}-bound`;
|
|
13
|
+
const boundVolume = `${boundProject}_data`;
|
|
14
|
+
const broker = `${holder}-broker`;
|
|
11
15
|
const marker = 'qa-private-secret-never-in-executor';
|
|
12
16
|
writeFileSync(join(dir, 'relay.env'), `TELEGRAM_BOT_TOKEN=${marker}\n`, { mode: 0o600 });
|
|
13
17
|
writeFileSync(join(dir, 'purpose.md'), 'Verify the packaged Ez runtime.\n', { mode: 0o644 });
|
|
@@ -48,6 +52,7 @@ try {
|
|
|
48
52
|
assert.throws(() => fs.openSync('/proc/'+process.ppid+'/mem', 'r'), {code:'EACCES'});
|
|
49
53
|
assert.throws(() => fs.readFileSync('/run/secrets/relay_env'), {code:'EACCES'});
|
|
50
54
|
assert.equal(fs.existsSync('/var/run/docker.sock'), false);
|
|
55
|
+
assert.match(fs.readFileSync('/proc/self/status','utf8'), /^NoNewPrivs:\\s+1$/m);
|
|
51
56
|
for (const pid of ['1', String(process.ppid)]) {
|
|
52
57
|
try { assert.ok(!fs.readFileSync('/proc/'+pid+'/environ','utf8').includes('${marker}')); }
|
|
53
58
|
catch (e) { if (e.code !== 'EACCES') throw e; }
|
|
@@ -70,6 +75,92 @@ try {
|
|
|
70
75
|
'--mount',`type=bind,src=${join(dir,'relay.env')},dst=/run/secrets/relay_env,readonly`,image,'exec','node','-e',customProbe]);
|
|
71
76
|
assert.equal(custom.status,0,custom.stderr);
|
|
72
77
|
assert.equal(JSON.parse(custom.stdout).uid,20001);
|
|
78
|
+
// Real Linux ownership, including pre-existing mode-600 control/registry
|
|
79
|
+
// files. Host bind mounts on desktop Docker can mask these failures.
|
|
80
|
+
const seed = run(['run','--rm','-v',`${privateVolume}:/qa`,'--entrypoint','node',image,'-e',`
|
|
81
|
+
const fs=require('fs');
|
|
82
|
+
for(const name of ['home','control','mind']) {
|
|
83
|
+
fs.mkdirSync('/qa/'+name,{recursive:true,mode:0o700});
|
|
84
|
+
fs.chownSync('/qa/'+name,20001,20002);
|
|
85
|
+
}
|
|
86
|
+
const files={
|
|
87
|
+
'/qa/control/control-state.json':{version:1,owner:null,pending:[]},
|
|
88
|
+
'/qa/home/config.json':{schemaVersion:1,workspace:'/qa/mind',hostConfig:'/qa/host-executor.json'},
|
|
89
|
+
'/qa/home/registry.json':{schemaVersion:1,owner:'/qa/home',plugins:{},commands:{}},
|
|
90
|
+
'/qa/host-executor.json':{isolation:'isolated',agents:[{name:'qa',toolsHome:'/qa/home',workspace:'/qa/mind',controlDir:'/qa/control'}]},
|
|
91
|
+
};
|
|
92
|
+
for(const [file,value] of Object.entries(files)) {
|
|
93
|
+
fs.writeFileSync(file,JSON.stringify(value),{mode:0o600});fs.chownSync(file,20001,20002);
|
|
94
|
+
}
|
|
95
|
+
`]);
|
|
96
|
+
assert.equal(seed.status,0,seed.stderr);
|
|
97
|
+
// Compose must reuse an existing operator-owned bind volume without
|
|
98
|
+
// recreating it or rejecting its driver options (legacy Compose v1 did).
|
|
99
|
+
const mountpoint = run(['volume','inspect','--format','{{.Mountpoint}}',privateVolume]).stdout.trim();
|
|
100
|
+
assert.ok(mountpoint.startsWith('/'));
|
|
101
|
+
const bind = run(['volume','create','--driver','local','--opt','type=none','--opt','o=bind','--opt',`device=${mountpoint}`,boundVolume]);
|
|
102
|
+
assert.equal(bind.status,0,bind.stderr);
|
|
103
|
+
const boundConfig = {services:{probe:{image,network_mode:'none',entrypoint:['node','-e',
|
|
104
|
+
`const fs=require('fs'),assert=require('assert/strict');assert.equal(JSON.parse(fs.readFileSync('/state/host-executor.json')).isolation,'isolated');console.log('bound-volume-preserved');`],
|
|
105
|
+
volumes:['data:/state:ro']}},volumes:{data:{}}};
|
|
106
|
+
const reused = spawnSync('docker',['run','--rm','-i','--network','none','-v','/var/run/docker.sock:/var/run/docker.sock',
|
|
107
|
+
'--entrypoint','docker-compose',image,'--project-name',boundProject,'--file','-','run','--rm','--no-deps','probe'],
|
|
108
|
+
{encoding:'utf8',timeout:60000,input:JSON.stringify(boundConfig)});
|
|
109
|
+
assert.equal(reused.status,0,reused.stderr);
|
|
110
|
+
assert.match(reused.stdout,/bound-volume-preserved/);
|
|
111
|
+
assert.equal(run(['volume','inspect','--format','{{index .Options "device"}}',boundVolume]).stdout.trim(),mountpoint);
|
|
112
|
+
const privateState = run(['run','--rm','-v',`${privateVolume}:/qa`,
|
|
113
|
+
'-e','EZ_CONTROL_DIR=/qa/control','-e','EZ_RUNTIME_UID=20001','-e','EZ_RUNTIME_GID=20002','-e','EZ_RELAY_UID=20003',
|
|
114
|
+
image,'owner','status']);
|
|
115
|
+
assert.equal(privateState.status,0,privateState.stderr);
|
|
116
|
+
assert.equal(JSON.parse(privateState.stdout).owner,null);
|
|
117
|
+
const brokerStarted = run(['run','-d','--name',broker,'--user','0:0','--cap-drop','ALL',
|
|
118
|
+
'--cap-add','DAC_OVERRIDE','--cap-add','CHOWN','--security-opt','no-new-privileges','--network','none',
|
|
119
|
+
'-v',`${privateVolume}:/qa`,'--entrypoint','node',image,'--import','/app/node_modules/tsx/dist/loader.mjs',
|
|
120
|
+
'/app/src/plugin-broker.mjs','--home','/qa/home','--workspace','/qa/mind','--control-dir','/qa/control',
|
|
121
|
+
'--socket','/qa/control/plugin-broker.sock','--host-config','/qa/host-executor.json']);
|
|
122
|
+
assert.equal(brokerStarted.status,0,brokerStarted.stderr);
|
|
123
|
+
let brokerReady=false;
|
|
124
|
+
for(let n=0;n<30;n++) {
|
|
125
|
+
const check=run(['exec','--user','20001:20002',broker,'node','-e',`
|
|
126
|
+
const fs=require('fs'),assert=require('assert/strict');
|
|
127
|
+
assert.ok(fs.statSync('/qa/control/plugin-broker.sock').isSocket());
|
|
128
|
+
const file='/qa/control/plugin-broker-plugins.json';
|
|
129
|
+
assert.equal(fs.statSync(file).uid,20001);assert.deepEqual(JSON.parse(fs.readFileSync(file)).plugins,[]);
|
|
130
|
+
`]);
|
|
131
|
+
if(check.status===0){brokerReady=true;break;}
|
|
132
|
+
await new Promise(resolve=>setTimeout(resolve,100));
|
|
133
|
+
}
|
|
134
|
+
assert.ok(brokerReady,run(['logs',broker]).stderr);
|
|
135
|
+
const prepare = run(['exec',broker,'node','--input-type=module','-e',`
|
|
136
|
+
import fs from 'node:fs/promises';
|
|
137
|
+
import {snapshot,install,prepareCommand} from '/app/src/plugins/manager.mjs';
|
|
138
|
+
const home='/qa/home',source='/qa/source';
|
|
139
|
+
await fs.mkdir(source);
|
|
140
|
+
const manifest={schemaVersion:1,id:'sample',version:'0.1.0',description:'Synthetic',commands:{sample:{executable:'client.mjs',args:[]}},skills:['SKILL.md']};
|
|
141
|
+
const deployment={schemaVersion:1,services:{sample:{buildTarget:'runtime',volumes:{},healthcheck:['node','--version']}},commands:{sample:{service:'sample',argv:['node','/app/client.mjs']}}};
|
|
142
|
+
for(const [name,value] of Object.entries({'package.json':JSON.stringify({files:['client.mjs','SKILL.md']}),'ez-plugin.json':JSON.stringify(manifest),'ez-deployment.json':JSON.stringify(deployment),'Dockerfile':'FROM scratch AS runtime','.dockerignore':'','client.mjs':'','SKILL.md':'Synthetic'}))await fs.writeFile(source+'/'+name,value);
|
|
143
|
+
// Only the Docker build effect is stubbed; real installation and private
|
|
144
|
+
// filesystem operations execute with the production broker capabilities.
|
|
145
|
+
await fs.writeFile('/qa/docker','#!/bin/sh\\nexit 0\\n',{mode:0o755});
|
|
146
|
+
process.env.PATH='/qa:'+process.env.PATH;
|
|
147
|
+
delete process.env.EZ_DOCKER_COMPOSE;
|
|
148
|
+
const pkg=await snapshot(source),config=JSON.parse(await fs.readFile(home+'/config.json'));
|
|
149
|
+
await install(home,config,'sample',source,pkg.revision);
|
|
150
|
+
const command=await prepareCommand(home,'sample',[],{invocation:true,environment:{}});
|
|
151
|
+
await command.release();
|
|
152
|
+
`]);
|
|
153
|
+
assert.equal(prepare.status,0,prepare.stderr);
|
|
154
|
+
const steward = run(['exec','--user','20001:20002',broker,'node','-e',`
|
|
155
|
+
const fs=require('fs'),assert=require('assert/strict');
|
|
156
|
+
for(const dir of ['packages','packages/sample','command-invocations']) {
|
|
157
|
+
const stat=fs.statSync('/qa/home/'+dir);
|
|
158
|
+
assert.equal(stat.uid,20001);assert.equal(stat.gid,20002);assert.equal(stat.mode&511,448);
|
|
159
|
+
fs.readdirSync('/qa/home/'+dir);
|
|
160
|
+
}
|
|
161
|
+
fs.readFileSync('/qa/home/packages/sample/compose.json');
|
|
162
|
+
`]);
|
|
163
|
+
assert.equal(steward.status,0,steward.stderr);
|
|
73
164
|
const failed = run(['run','--rm',image,'exec','node','-e','process.exit(23)']);
|
|
74
165
|
assert.equal(failed.status, 23, failed.stderr);
|
|
75
166
|
// The live delivery socket is the single-relay guard (it replaced the kernel
|
|
@@ -108,4 +199,4 @@ try {
|
|
|
108
199
|
}
|
|
109
200
|
assert.equal(released,0,'a dead relay must release the delivery socket');
|
|
110
201
|
console.log('Docker smoke passed: direct non-root application help/start/health/stop, inherited private descriptor, non-root executor, isolated Codex on PATH, private secret isolation, literal argv, exit code, no Docker socket, live delivery-socket guard and crash release.');
|
|
111
|
-
} finally { run(['rm','-f',holder,application]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
|
|
202
|
+
} finally { run(['rm','-f',holder,application,broker]); run(['volume','rm',boundVolume,volume,privateVolume]); rmSync(dir, {recursive:true, force:true}); }
|
|
@@ -296,8 +296,9 @@ separate CLI histories. A retry cannot change an admitted turn's AI choice.
|
|
|
296
296
|
|
|
297
297
|
Register owner-chat channels with `--share-owner` and submit `followOwner:true`.
|
|
298
298
|
Web, phone and Telegram then use the same selected native conversation and AI.
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
Model and reasoning changes on the same client and provider preserve the native
|
|
300
|
+
conversation; client or provider changes start a separate conversation.
|
|
301
|
+
Already-queued work keeps its captured AI choice.
|
|
301
302
|
This works without Telegram and requires no transcript replay or new runner.
|
|
302
303
|
Scoped application conversations remain separate when the flag is omitted.
|
|
303
304
|
Owner identity does not automatically merge histories or grant learner access.
|
|
@@ -444,3 +445,6 @@ partial request. Separate owner runtimes must retain separate workspace mounts.
|
|
|
444
445
|
|
|
445
446
|
This is inbound file support. Application replies currently deliver text only;
|
|
446
447
|
outgoing file downloads remain unsupported by this HTTP channel.
|
|
448
|
+
|
|
449
|
+
Private scope model selections are stored on that scope’s native-session binding.
|
|
450
|
+
They do not add entries to the owner’s saved AI menu or consume its preset limit.
|
|
@@ -7,10 +7,13 @@ No provider SDK, model fallback, history migration, or second execution loop.
|
|
|
7
7
|
At intake each journal entry receives an immutable preset and conversation ID.
|
|
8
8
|
Batches cannot cross that boundary. The run copies that choice and invokes the
|
|
9
9
|
native adapter with its exact model/effort. A later menu change cannot reroute it.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Each named conversation saves its model and reasoning level. Switching back through
|
|
11
|
+
`/chats` restores that choice and the exact native session, including after restart.
|
|
12
|
+
`/ai` shows the current choice and saves changes immediately. Changing model or
|
|
13
|
+
reasoning level on the same client and provider preserves the conversation's name
|
|
14
|
+
and native history; subsequent turns pass the new settings to the native engine.
|
|
15
|
+
Changing client or provider starts a separate native conversation. Old bindings
|
|
16
|
+
remain available so accepted work can finish and owners can reopen their chats.
|
|
14
17
|
|
|
15
18
|
Grok/Claude accept caller-selected native UUIDs. Codex/OpenCode generate IDs, so
|
|
16
19
|
the adapter reads only their typed JSONL session metadata; stdout never becomes a
|
|
@@ -9,13 +9,17 @@ marking it internal does not confer owner authority.
|
|
|
9
9
|
## Messaging v1
|
|
10
10
|
|
|
11
11
|
The owner asks the agent to contact one person for a bounded purpose. The owner
|
|
12
|
-
agent
|
|
12
|
+
agent uses `ezenciel-agents-task start`: registered source,
|
|
13
13
|
exact canonical contact, purpose, explicitly shareable context, and expiry (up to
|
|
14
|
-
72 hours).
|
|
14
|
+
72 hours). A current authenticated owner turn can start work already authorized
|
|
15
|
+
by that request, without another approval click. The native engine interprets
|
|
16
|
+
the request; core does not infer authority from keywords. `propose` remains for
|
|
17
|
+
scope needing a new owner decision. Scheduled and external runs cannot use either.
|
|
18
|
+
The core binds the grant
|
|
15
19
|
to the verified owner, current source registration, and connected account. The
|
|
16
20
|
owner does not edit JSON. The agent uses `list` and `revoke` when asked.
|
|
17
21
|
|
|
18
|
-
After
|
|
22
|
+
After activation the relay starts a restricted task, including the initial outgoing
|
|
19
23
|
message. Incoming-only tasks instead wait for new correspondence and never create
|
|
20
24
|
an opening run. Their task records use version 2 so older task readers fail closed. Matching new correspondence resumes that task in a fresh native session.
|
|
21
25
|
Other contacts remain blocked unless an owner-approved any-conversation grant is
|
|
@@ -25,6 +29,13 @@ settings changes, or access to owner memory. A contact can have one active or
|
|
|
25
29
|
pending task at a time. Completed, revoked, expired, replaced-source, and changed-
|
|
26
30
|
account grants cannot dispatch further messages.
|
|
27
31
|
|
|
32
|
+
Activated grants persist in the protected task record with a digest of all
|
|
33
|
+
immutable scope fields. Transient approval requests are not the continuing
|
|
34
|
+
authority store. Owner replacement, scope mutation, expiry and revocation still
|
|
35
|
+
invalidate authorization. The initial dispatch marker also persists: relay
|
|
36
|
+
restart does not replay an uncertain opener. Pending or legacy records without
|
|
37
|
+
a grant need their original live approval and are never auto-authorized.
|
|
38
|
+
|
|
28
39
|
The worker receives only the approved dossier, its notes, its operation receipts,
|
|
29
40
|
and rechecked correspondence for its contact. All dossier contents may be shared
|
|
30
41
|
with that contact. The agent judges how to pursue the purpose; code does not prove
|
|
@@ -55,7 +66,7 @@ exact registered `ez` command plus fixed arguments ending in literal
|
|
|
55
66
|
`-- {input}`. The restricted broker replaces only `{input}` with the
|
|
56
67
|
correspondent's string; the model cannot select a command, prepend flags, choose
|
|
57
68
|
a recipient, access a shell or change the fixed arguments. The full command
|
|
58
|
-
vector and disclosure scope
|
|
69
|
+
vector and disclosure scope are bound by the immutable owner grant. Core rechecks
|
|
59
70
|
the grant immediately before and after invocation and before every reply. The
|
|
60
71
|
installed alias must declare `channelQuery: true` and explicitly declare that it
|
|
61
72
|
accepts external input without external sends, record changes, or interactive
|
package/docs/docker-runtime.md
CHANGED
|
@@ -22,6 +22,8 @@ on the same CLI. Each agent gets its own purpose and native conversation IDs.
|
|
|
22
22
|
`isolated` sets `EZ_EXECUTOR_TRANSPORT=local` and runs the native CLI in the
|
|
23
23
|
relay. `host-capable` sets `host` and reuses the installer UID; unlabeled
|
|
24
24
|
existing host transports stay host-capable so they are not flipped.
|
|
25
|
+
Only isolated deployments configure the plugin-broker socket and Compose profile;
|
|
26
|
+
host-capable deployments use their bound host transport and registry.
|
|
25
27
|
|
|
26
28
|
On Docker Desktop/OrbStack the control volume's Unix socket is not connectable
|
|
27
29
|
from the macOS host, so a host-capable agent also publishes an authenticated
|
|
@@ -84,6 +86,22 @@ only service allowed to launch plugin containers. The relay never receives the
|
|
|
84
86
|
Docker socket or a tools registry mount. Host-capable agents keep the existing
|
|
85
87
|
host executor path instead.
|
|
86
88
|
|
|
89
|
+
The trusted broker has only `DAC_OVERRIDE` and `CHOWN` in addition to its
|
|
90
|
+
Docker socket, so it can read private installer-owned registry files and preserve
|
|
91
|
+
their ownership. The relay drops all capabilities before loading its secrets.
|
|
92
|
+
It retains distinct real/effective UIDs; `no_new_privs` is applied to the native
|
|
93
|
+
executor after normalizing those IDs. Applying it earlier can cause Linux
|
|
94
|
+
security modules to discard the relay's effective UID during exec.
|
|
95
|
+
Private native credentials and skills are checked using that effective UID;
|
|
96
|
+
`access(2)` checks the distinct real UID and can incorrectly hide a valid binding.
|
|
97
|
+
Runtime updates preserve explicit relay build arguments from the operator's
|
|
98
|
+
Compose deployment, including native CLI version pins.
|
|
99
|
+
New private broker directories and update archives inherit the installer's
|
|
100
|
+
ownership. Existing installations need the reviewed deployment migration in
|
|
101
|
+
[application-channel.md](application-channel.md#reviewed-deployment-migration)
|
|
102
|
+
for this Compose capability change; ordinary updates retain their compatibility
|
|
103
|
+
guard. Preserve the existing runtime UID/GID and private state during migration.
|
|
104
|
+
|
|
87
105
|
## Operate and verify
|
|
88
106
|
|
|
89
107
|
Root-started containers default to executor UID/GID `1000:1000` and relay real
|
|
@@ -164,6 +182,9 @@ an already connected account. Monitoring and sends require their own authority.
|
|
|
164
182
|
|
|
165
183
|
### Isolated plugin commands
|
|
166
184
|
|
|
185
|
+
The image pins modern Docker Compose, including its standalone executable, so
|
|
186
|
+
existing operator-created volumes retain their data and driver bindings.
|
|
187
|
+
|
|
167
188
|
The relay-side `ez` client sends only declared aliases, literal argument arrays,
|
|
168
189
|
bounded stdin, and the active run ID to the broker. The broker re-reads owner
|
|
169
190
|
authority and the registry before every call, pins the discovered revision, and
|
|
@@ -176,8 +197,10 @@ Source-based `plugins inspect`, `catalog-add`, and `install` management calls ar
|
|
|
176
197
|
accepted only for an existing path inside that agent's mounted workspace, with a
|
|
177
198
|
literal SHA-256 revision where the command requires one. The same management
|
|
178
199
|
surface permits data-preserving `plugins uninstall` and local candidate
|
|
179
|
-
`updates prepare/apply/recover`; candidate archives must also be inside the
|
|
180
|
-
workspace.
|
|
200
|
+
plugin `updates prepare/apply/recover`; plugin candidate archives must also be inside the
|
|
201
|
+
workspace. Local main-runtime archives require operator preparation outside the
|
|
202
|
+
agent, because the runtime contains the privileged broker. The agent can apply
|
|
203
|
+
an operator-prepared candidate. Public update discovery stays with the host supervisor because the
|
|
181
204
|
broker has no network. The broker never turns an arbitrary host path into a
|
|
182
205
|
plugin mount.
|
|
183
206
|
|
package/docs/local-qa.md
CHANGED
|
@@ -6,6 +6,11 @@ an immutable candidate in the PA's local tools directory. The agent reads its
|
|
|
6
6
|
manifest and uses `ez updates prepare main --file ...` and `apply` on the owner's
|
|
7
7
|
request. Automatic public update policy stays unchanged.
|
|
8
8
|
|
|
9
|
+
For isolated agents, the operator prepares main-runtime archives through the
|
|
10
|
+
host-bound launcher outside the agent. The isolated agent may apply that prepared
|
|
11
|
+
job; its broker rejects agent-supplied main-runtime archives. Plugin candidates
|
|
12
|
+
retain the workspace-scoped preparation flow below.
|
|
13
|
+
|
|
9
14
|
Commit, push and open a PR first — then stage only from that PR's exact commit. The staging script refuses dirty checkouts, so uncommitted work can never become a candidate. If QA finds issues, fix forward in the same PR (or a new PR) and stage a new versioned candidate; never leave a QA-only copy lingering outside the PR:
|
|
10
15
|
|
|
11
16
|
```sh
|
package/docs/plugin-guide.md
CHANGED
|
@@ -57,7 +57,16 @@ At minimum, provide:
|
|
|
57
57
|
machine-readable commands, stable exit codes, and readback for writes.
|
|
58
58
|
|
|
59
59
|
The manager snapshots the package according to `package.json.files` plus the
|
|
60
|
-
required manifests and Docker inputs.
|
|
60
|
+
required manifests and Docker inputs. `ez tools list --details` includes
|
|
61
|
+
`skillReads`: literal command argument arrays for reading each installed skill.
|
|
62
|
+
Use `ez tools skill <plugin> <declared-path>` to return its text and pinned
|
|
63
|
+
revision. The isolated executor cannot open private host package paths directly;
|
|
64
|
+
the authorized broker reads only manifest-declared regular skill files, without
|
|
65
|
+
copying them into the agent workspace. Skill reads reject symlinks and files over
|
|
66
|
+
64 KiB. The existing generic discovery footer remains accurate; no plugin-specific
|
|
67
|
+
usage snippet or per-turn instruction is needed for this read contract.
|
|
68
|
+
|
|
69
|
+
Keep one canonical checkout; do not keep
|
|
61
70
|
permanent packaged copies or a second registry. See the full
|
|
62
71
|
[plugin contribution requirements](plugin-contributions.md) and the
|
|
63
72
|
[plugin manager contract](plugins.md#deployment-descriptors).
|
|
@@ -199,4 +208,3 @@ hash.
|
|
|
199
208
|
|
|
200
209
|
For contribution, security, and removal details, follow the links above and the
|
|
201
210
|
plugin's own `CONTRIBUTING.md`, `SECURITY.md`, and README.
|
|
202
|
-
|
package/docs/plugins.md
CHANGED
|
@@ -29,7 +29,14 @@ initial main onboarding; no sibling repository, broker or provider account is
|
|
|
29
29
|
needed. Finish owner pairing and verify an actual Telegram agent reply first.
|
|
30
30
|
Init creates a private registry and `tools/bin/ez`, adds a
|
|
31
31
|
managed AGENTS.md registry locator, and binds the matching host executor to that bin
|
|
32
|
-
folder. Native binaries are linked through;
|
|
32
|
+
folder. Native binaries are linked through; the package's own `bin/ez` is replaced
|
|
33
|
+
by that bound launcher, while an unrelated existing `ez` collision fails.
|
|
34
|
+
Both host-bound and isolated `ez --help` list installed native scheduling and delivery helpers
|
|
35
|
+
under `native`, with literal `argv` entries for their help. These are existing
|
|
36
|
+
native executables, not plugin aliases; discovery does not grant authority.
|
|
37
|
+
Standalone registries omit helpers they do not have installed. The managed
|
|
38
|
+
workspace locator remains unchanged: core help discovers native controls and
|
|
39
|
+
`tools list --details` discovers registered plugins.
|
|
33
40
|
Run before starting the host executor. For an already running installation,
|
|
34
41
|
place a symlink to the returned launcher in that agent's existing private bin
|
|
35
42
|
directory and set that binding's `toolsHome` to the absolute registry directory.
|
|
@@ -52,6 +59,10 @@ For an isolated Codex agent, this binding is consumed by the per-agent
|
|
|
52
59
|
`plugin-broker` service, not by a host executor. Start the deployment only after
|
|
53
60
|
initialization with `COMPOSE_PROFILES=isolated`; Compose then starts the relay
|
|
54
61
|
and broker together. The relay-side `ez` command has no tools-home or Docker
|
|
62
|
+
access. Operator-owned private network grants also work through the broker:
|
|
63
|
+
each grant binds an exact reviewed plugin revision and declared service, and
|
|
64
|
+
never attaches the relay or broker itself to that network. The host binding
|
|
65
|
+
must remain outside the agent's writable mounts. The client has no Docker
|
|
55
66
|
socket mount. It sends a declared alias, literal arguments, bounded stdin and
|
|
56
67
|
the active run ID over the private control-directory Unix socket. The broker
|
|
57
68
|
revalidates the exact registry/host/workspace ownership, pinned revision and
|
|
@@ -68,7 +79,10 @@ across agents unless an operator adds an explicit reviewed binding.
|
|
|
68
79
|
The same broker accepts source-based `plugins inspect`, `catalog-add`, and
|
|
69
80
|
`install` only when the source is an existing path under the bound workspace and
|
|
70
81
|
the revision is literal and pinned. It also exposes data-preserving
|
|
71
|
-
`plugins uninstall` and the local-file `updates prepare/apply/recover` lifecycle.
|
|
82
|
+
`plugins uninstall` and the local-file plugin `updates prepare/apply/recover` lifecycle.
|
|
83
|
+
Local main-runtime archives require operator preparation outside the agent:
|
|
84
|
+
the agent cannot author replacement code for its privileged broker. It may
|
|
85
|
+
apply an already prepared operator candidate through the normal update queue.
|
|
72
86
|
Candidate archives must be staged inside the bound workspace; public registry
|
|
73
87
|
discovery remains host-supervisor work because the broker has no network. Paths
|
|
74
88
|
outside that workspace are rejected.
|
|
@@ -386,6 +400,12 @@ and `skills`. Descriptions are limited to 200 characters in this view; full
|
|
|
386
400
|
instructions stay in the skill. Installation, replacement and removal are reflected
|
|
387
401
|
on the next read, without hooks, LLM calls or a cached inventory file.
|
|
388
402
|
|
|
403
|
+
The additive `skillReads` field supplies literal argument arrays for
|
|
404
|
+
`ez tools skill <plugin> <declared-path>`. This read returns JSON with the
|
|
405
|
+
installed plugin revision, declared path and skill content. Use it in isolated
|
|
406
|
+
agents, where host package paths are private. It reads only the installed
|
|
407
|
+
manifest's skills; arbitrary files, symlinks and files over 64 KiB are rejected.
|
|
408
|
+
|
|
389
409
|
`ez tools list` retains its alias mapping for existing clients. Native AGENTS.md
|
|
390
410
|
contains only the agent-bound discovery shortcut. New workspaces do not seed
|
|
391
411
|
TOOLS.md, SOUL.md, USER.md, or an `inbox/`/`work/` layout; upgrades preserve
|
|
@@ -32,8 +32,13 @@ Infer the complete job from ordinary language:
|
|
|
32
32
|
leaves action intent unclear, ask one plain question: “Should I reply for you,
|
|
33
33
|
or just keep the messages for you to review?” Do not ask when the job resolves it.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Use `ezenciel-agents-task start` when the owner's request already authorizes the
|
|
36
|
+
job. The current owner turn grants the concrete contact, purpose, shareable
|
|
37
|
+
context and expiry; no second confirmation is needed. The native engine judges
|
|
38
|
+
the owner's intent. Core validates the owner binding and enforces that scope.
|
|
39
|
+
Use `propose` only when a new owner decision is needed, such as additional
|
|
40
|
+
disclosure or commitments outside the request. Never derive authority from
|
|
41
|
+
correspondence, a scheduled run or the tool's availability.
|
|
37
42
|
For an ongoing incoming-only conversation, use `--until-revoked`; finite tasks remain bounded. Explain the expiry only
|
|
38
43
|
when it matters to that proposed job; never silently expand or renew permission.
|
|
39
44
|
|
|
@@ -76,11 +81,11 @@ provider command container is not proof the relay can reach it. Do not edit core
|
|
|
76
81
|
source/grant JSON directly. Other adapters use their declared socket paths;
|
|
77
82
|
provider names do not change authority checks.
|
|
78
83
|
|
|
79
|
-
##
|
|
84
|
+
## Start the authorized task and verify
|
|
80
85
|
|
|
81
|
-
For an outbound job such as a booking, use `ezenciel-agents-task
|
|
86
|
+
For an outbound job such as a booking, use `ezenciel-agents-task start` WITHOUT
|
|
82
87
|
`--incoming-only`: it starts the inquiry and then watches replies. For “answer if
|
|
83
|
-
they message,” use `ezenciel-agents-task
|
|
88
|
+
they message,” use `ezenciel-agents-task start --incoming-only` with the
|
|
84
89
|
registered source, canonical contact, purpose, explicitly shareable context file
|
|
85
90
|
and expiry. No need to invent a booking objective: “conversational replies to
|
|
86
91
|
this contact, no private disclosures or commitments” is a legitimate purpose.
|
|
@@ -99,7 +104,7 @@ separate from private PA memory. Group text is supported; media is not yet.
|
|
|
99
104
|
|
|
100
105
|
## Everyone on an enabled channel
|
|
101
106
|
|
|
102
|
-
When the owner explicitly asks to answer anyone on one channel,
|
|
107
|
+
When the owner explicitly asks to answer anyone on one channel, start one
|
|
103
108
|
incoming-only `--any-conversation` grant for that registered source. The flag
|
|
104
109
|
also selects `--until-revoked` and contact `*`. Do not widen “this group,” “this
|
|
105
110
|
person,” or “customers in this list” into everyone. The source must support a
|
|
@@ -129,16 +134,16 @@ Library source:
|
|
|
129
134
|
The registered alias must declare `channelQuery: true` with explicit exposure
|
|
130
135
|
metadata showing that it accepts external input but cannot send, change records,
|
|
131
136
|
or require interactive review. Core verifies that installed declaration on every
|
|
132
|
-
invocation. Verify the selected source and query alias first, then
|
|
137
|
+
invocation. Verify the selected source and query alias first, then start:
|
|
133
138
|
|
|
134
139
|
```sh
|
|
135
|
-
ezenciel-agents-task
|
|
140
|
+
ezenciel-agents-task start --source telegram --any-conversation \
|
|
136
141
|
--purpose "Answer questions using approved public knowledge" \
|
|
137
142
|
--context-file work/public-answer-policy.txt \
|
|
138
143
|
--capability-file work/public-answer-capabilities.json
|
|
139
144
|
```
|
|
140
145
|
|
|
141
|
-
The
|
|
146
|
+
The grant binds the audience, disclosure context and exact command vectors.
|
|
142
147
|
Command output may be shown to anyone in that audience. External writes need
|
|
143
148
|
their own bounded tool contract and receipts. Revocation stops new capture,
|
|
144
149
|
cancels queued replies and rechecks every tool/send. Public Telegram groups must
|
|
@@ -147,17 +152,26 @@ and the source-wide public backlog is bounded. Core admits at most 60 public
|
|
|
147
152
|
conversations and 60 replies per hour. A grant revokes after 1,000 conversations
|
|
148
153
|
or replies so the owner must review continued broad access.
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
`start` returns active only after the provider accepts the watch and core saves
|
|
156
|
+
the scoped grant. `propose` instead presents the exact scope for owner confirmation.
|
|
157
|
+
Ordinary messages inside an active grant need no repeated confirmations. Incoming-only grants create
|
|
152
158
|
no initial run or opening message. They remain active across replies until expiry
|
|
153
159
|
or owner revocation; the worker cannot close a watch by calling `complete`.
|
|
154
|
-
After handling a message, save a task note and finish the run. After
|
|
160
|
+
After handling a message, save a task note and finish the run. After activation, verify active core state,
|
|
155
161
|
contact and expiry, source reachability, and the provider's task watch. With
|
|
156
162
|
`--incoming-only`, an empty conversation is correctly idle until a new message.
|
|
157
163
|
If approval is still pending, say pending; if setup failed, name the actual
|
|
158
164
|
failure and continue repair within scope. Never report “active” based on notes,
|
|
159
165
|
installation, connected status or subscription alone.
|
|
160
166
|
|
|
167
|
+
Active grants survive relay restarts independently of transient approval messages.
|
|
168
|
+
An initial outgoing run is dispatched at most once; a restart may lose an in-flight
|
|
169
|
+
run, and core never blindly repeats its opener. New matching correspondence can
|
|
170
|
+
still resume the task. Legacy tasks without a persisted grant still require their
|
|
171
|
+
original live approval; if that was lost on restart, revoke and recreate within
|
|
172
|
+
the owner's existing authority. Pending proposals never become active merely
|
|
173
|
+
because their approval record disappeared.
|
|
174
|
+
|
|
161
175
|
After an authorized test message, inspect the corresponding task run and send
|
|
162
176
|
receipt and compare recipient readback. An accepted send is not proof of delivery.
|
|
163
177
|
After revocation/expiry no subsequent reply may dispatch. Keep these checks
|
package/docs/upgrades.md
CHANGED
|
@@ -164,7 +164,11 @@ applying job before resuming normal work. A failed build leaves the old runtime
|
|
|
164
164
|
running. Failed activation restores the previous code/configuration and checks it.
|
|
165
165
|
If recovery itself fails, inspect and fix the reported infrastructure problem,
|
|
166
166
|
then use `ez updates recover <job-id>` and finish the turn. This retries only the
|
|
167
|
-
saved code/configuration recovery, not provider operations.
|
|
167
|
+
saved code/configuration recovery, not provider operations. If the original relay
|
|
168
|
+
is already running and healthy with the exact saved immutable image, recovery
|
|
169
|
+
keeps it while restoring the host and bindings. This also works when Docker
|
|
170
|
+
has removed that image index but retained the original container. Other images
|
|
171
|
+
and unhealthy relays still require the normal Compose replacement.
|
|
168
172
|
An update never wakes the agent or injects wrapper state into its session; the
|
|
169
173
|
owner or agent inspects receipts with `ez updates` when asked.
|
|
170
174
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jc_stack/ez-agents",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.44",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
|
package/src/agent-install.ts
CHANGED
|
@@ -79,9 +79,11 @@ export const createAgent = async (options: {
|
|
|
79
79
|
EZ_AGENT_WORKSPACE: join(deploymentDir, 'mind'),
|
|
80
80
|
EZ_CONTROL_DIR: join(deploymentDir, 'control'),
|
|
81
81
|
EZ_TOOLS_HOME: join(deploymentDir, 'tools'),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
...(isolation === 'isolated' ? {
|
|
83
|
+
EZ_PLUGIN_BROKER_SOCKET: join(deploymentDir, 'control', 'plugin-broker.sock'),
|
|
84
|
+
EZ_PLUGIN_BROKER_HOST_CONFIG: join(deploymentDir, 'host-executor.json'),
|
|
85
|
+
COMPOSE_PROFILES: 'isolated',
|
|
86
|
+
} : {}),
|
|
85
87
|
EZ_WHATSAPP_IPC_VOLUME: `${project}-whatsapp-ipc`,
|
|
86
88
|
EZ_WHATSAPP_CLIENT_VOLUME: `${project}-whatsapp-client`,
|
|
87
89
|
}
|
package/src/ai-cli.ts
CHANGED
|
@@ -2,18 +2,27 @@ import { parseArgs } from 'node:util'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
|
-
import { readModels, validateSelection, type AiPreset } from './ai.js'
|
|
5
|
+
import { readModels, presetProvider, validateSelection, type AiPreset } from './ai.js'
|
|
6
6
|
import { ControlStore } from './control-state.js'
|
|
7
7
|
|
|
8
8
|
const {values,positionals}=parseArgs({allowPositionals:true,options:{help:{type:'boolean'},cli:{type:'string'},provider:{type:'string'},model:{type:'string'},effort:{type:'string'}}})
|
|
9
9
|
if(values.help){
|
|
10
|
-
console.log('Usage: ezenciel-agents-ai list | select --cli <installed-cli> [--provider <provider>] [--model <model>] [--effort <effort>]\nChoose only values returned by list. Selection affects subsequent messages;
|
|
10
|
+
console.log('Usage: ezenciel-agents-ai list | select --cli <installed-cli> [--provider <provider>] [--model <model>] [--effort <effort>] | default\nChoose only values returned by list. Selection affects subsequent messages; default makes the selected AI the choice for new conversations. Queued work is unchanged.')
|
|
11
11
|
process.exit(0)
|
|
12
12
|
}
|
|
13
13
|
if (!process.env.EZ_CONTROL_DIR) throw new Error('Use this agent’s bound control directory')
|
|
14
14
|
const catalog=await readFile(join(process.env.EZ_CONTROL_DIR,'host-executor','models.json'),'utf8')
|
|
15
15
|
.then(text=>JSON.parse(text)).catch(()=>readModels(undefined,undefined,join(process.env.EZ_CONTROL_DIR!,'cli','codex'),undefined,undefined,undefined,process.env.EZ_CONTROL_DIR))
|
|
16
16
|
if(positionals[0]==='list')console.log(JSON.stringify(catalog))
|
|
17
|
+
else if(positionals[0]==='default'){
|
|
18
|
+
const control=new ControlStore(process.env.EZ_CONTROL_DIR,900000)
|
|
19
|
+
const state=await control.status()
|
|
20
|
+
const selected=state.ai?.presets.find(p=>p.id===state.ai?.selectedId)
|
|
21
|
+
if(!selected)throw new Error('No selected AI')
|
|
22
|
+
await validateSelection(selected,catalog)
|
|
23
|
+
await control.defaultPreset(selected.id)
|
|
24
|
+
console.log(JSON.stringify({default:selected,applies:'new conversations; current and queued work unchanged'}))
|
|
25
|
+
}
|
|
17
26
|
else if(positionals[0]==='select'){
|
|
18
27
|
const preset:AiPreset={id:randomBytes(8).toString('hex'),name:[values.provider,values.model||values.cli,values.effort].filter(Boolean).join(' · '),cli:values.cli||'',provider:values.provider,model:values.model,effort:values.effort}
|
|
19
28
|
await validateSelection(preset,catalog)
|
|
@@ -24,6 +33,6 @@ else if(positionals[0]==='select'){
|
|
|
24
33
|
const selected=existing||preset
|
|
25
34
|
if(!existing)await control.savePreset(selected)
|
|
26
35
|
const current=state.ai.presets.find(p=>p.id===state.ai!.selectedId)!
|
|
27
|
-
await control.selectPreset(selected.id,state.activeSession?.sessionId??null,current.cli!==selected.cli||current
|
|
36
|
+
await control.selectPreset(selected.id,state.activeSession?.sessionId??null,current.cli!==selected.cli||presetProvider(current)!==presetProvider(selected)||Boolean(state.activeSession&&!state.activeSession.cli))
|
|
28
37
|
console.log(JSON.stringify({selected,defaultUnchanged:true,applies:'subsequent messages; queued work keeps its captured choice'}))
|
|
29
|
-
}else throw new Error('Use list
|
|
38
|
+
}else throw new Error('Use list, select --cli <installed-cli> [--provider <provider>] [--model <model>] [--effort <effort>], or default')
|