@jc_stack/ez-agents 0.1.0-beta.47 → 0.1.0-beta.49
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/.env.example +3 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +30 -0
- package/Dockerfile +5 -1
- package/THIRD_PARTY_NOTICES.md +6 -3
- package/bin/ez +7 -0
- package/docker/broker-readiness.mjs +54 -0
- package/docker/broker-update-smoke.mjs +100 -0
- package/docker/run.ts +1 -1
- package/docker/upgrade-smoke.mjs +1 -1
- package/docs/application-channel.md +32 -1
- package/docs/architecture/ai-selection.md +23 -4
- package/docs/docker-runtime.md +57 -0
- package/docs/isolated-plugin-update-readiness.md +66 -0
- package/docs/releasing.md +4 -0
- package/docs/scheduling.md +141 -6
- package/docs/selective-monitoring.md +13 -0
- package/docs/upgrades.md +33 -4
- package/docs/work-preflight.md +9 -0
- package/package.json +1 -1
- package/src/ai-cli.ts +28 -15
- package/src/ai.ts +98 -15
- package/src/application-channel.ts +52 -12
- package/src/application-cli.ts +7 -1
- package/src/application-origin.ts +2 -0
- package/src/audio.ts +37 -22
- package/src/auth-profile.ts +53 -0
- package/src/client-defaults.ts +3 -2
- package/src/codex-session.ts +14 -3
- package/src/config.ts +9 -0
- package/src/control-state.ts +31 -22
- package/src/delivery-socket-client.d.mts +2 -0
- package/src/delivery-socket-client.mjs +2 -0
- package/src/delivery-socket.ts +26 -1
- package/src/event-sources.ts +2 -2
- package/src/executor.ts +81 -7
- package/src/files.ts +47 -3
- package/src/host-executor.ts +68 -34
- package/src/index.ts +227 -110
- package/src/install-tools.mjs +8 -5
- package/src/menu.ts +9 -7
- package/src/plugin-broker.mjs +50 -14
- package/src/plugins/manager.mjs +34 -8
- package/src/run-evidence.ts +78 -0
- package/src/runs.ts +24 -2
- package/src/schedule-cli.ts +100 -13
- package/src/schedule-preflight.ts +23 -0
- package/src/scheduled-tasks.ts +16 -12
- package/src/scheduler.ts +85 -19
- package/src/scripts.ts +167 -0
- package/src/setup-fallback.ts +122 -0
- package/src/setup.ts +2 -2
- package/src/task-capability.ts +17 -1
- package/src/task-executor.ts +13 -9
- package/src/task-mcp.ts +20 -8
- package/src/tasks.ts +89 -9
- package/src/updates/runtime.mjs +107 -30
- package/src/updates/supervisor.mjs +27 -6
- package/test/ai.test.ts +40 -0
- package/test/application-channel.test.ts +16 -1
- package/test/audio.test.ts +19 -0
- package/test/auth-profiles.test.ts +187 -0
- package/test/config.test.ts +8 -0
- package/test/executor.test.ts +37 -0
- package/test/files.test.ts +44 -0
- package/test/host-executor.test.ts +45 -2
- package/test/install-json.test.mjs +21 -0
- package/test/plugin-broker.test.mjs +67 -0
- package/test/plugin-manager.test.mjs +44 -0
- package/test/private-speech-config.test.ts +22 -0
- package/test/run-evidence.test.ts +78 -0
- package/test/schedule-preflight.test.ts +49 -0
- package/test/scheduler-host.test.ts +7 -0
- package/test/scheduler-relay.test.ts +38 -3
- package/test/scheduler.test.ts +1 -1
- package/test/script-schedules.test.ts +219 -0
- package/test/setup-fallback.test.ts +245 -0
- package/test/task-native.test.ts +50 -12
- package/test/tasks.test.ts +97 -4
- package/test/updates.test.mjs +100 -11
package/.env.example
CHANGED
|
@@ -13,6 +13,9 @@ EZ_AGENT_WORKSPACE=./agent
|
|
|
13
13
|
# Optional. Default is 300.
|
|
14
14
|
# Agent execution has no wall-clock timeout. Use /stop or schedule cancel.
|
|
15
15
|
|
|
16
|
+
# Maximum concurrent owner-scheduled jobs; foreground chat keeps a separate serial lane.
|
|
17
|
+
# EZ_SCHEDULED_CONCURRENCY=6
|
|
18
|
+
|
|
16
19
|
# CLI Executor adapter. Supported: codex (default), codex-gui, claude, grok, opencode, pi
|
|
17
20
|
# Switch easily with: pnpm run setup <executor-name>
|
|
18
21
|
EZ_EXECUTOR_CLI=codex
|
package/AGENTS.md
CHANGED
|
@@ -120,8 +120,8 @@ round merely to seek more criticism, tests or hypothetical edge cases.
|
|
|
120
120
|
- **Rate limits:** Avoid spamming bubbles to respect Telegram’s 1 message/sec per-chat rate limit.
|
|
121
121
|
|
|
122
122
|
## 6. Concurrency & Workspace Invariants
|
|
123
|
-
- **Responsive owner channels:** One foreground turn and
|
|
124
|
-
- The delivery socket carries every cross-process enqueue/receipt, never control/ files. Host plugin workspace invocations remain excluded until
|
|
123
|
+
- **Responsive owner channels:** One foreground turn and up to `EZ_SCHEDULED_CONCURRENCY` owner-scheduled turns may run concurrently (default six); foreground stays serial. Separate native sessions isolate history and engine state, while the agent-owned Markdown workspace is shared. The engine coordinates shared file updates and plugins guard canonical record writes. Do not add a second reply agent, scheduler or execution queue.
|
|
124
|
+
- The delivery socket carries every cross-process enqueue/receipt, never control/ files. Host plugin workspace invocations remain excluded until all native turns finish. Scheduled jobs keep separate native CLI sessions and control-side task state. Restricted contact tasks retain their separate authorization boundary. Delegation decisions and goal persistence belong to the agent/executor; there is no automatic planner or canned chat ACK. Production executor runs have no wall-clock timeout; cancellation is explicit.
|
|
125
125
|
|
|
126
126
|
## 7. Fail-Closed Authority (Channel Access ≠ Execution)
|
|
127
127
|
- Incoming messages outside the approved owner binding must **never** spawn the executor. A first DM or group message records a pending request only. Explicitly approved owner groups grant owner access to all human members in that exact chat; bots and anonymous posts are ignored.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.49
|
|
4
|
+
|
|
5
|
+
- Upgrade compatible plugins while native work continues, draining in-flight plugin calls for activation and preserving rollback. Core updates still wait for idle.
|
|
6
|
+
- Keep separate named Codex and Claude native login homes, with account-bound conversations, profile-aware model selection and fail-closed admission.
|
|
7
|
+
- Try ordered saved setups for a scheduled task only after a typed failure before work begins. Hooks, model/tool activity, access denial, cancellation and uncertain transport stop fallback; quota rejection skips the same native CLI/provider scope.
|
|
8
|
+
- Preserve each scheduled attempt's captured auth profile and admit only the active setup on the host transport.
|
|
9
|
+
|
|
10
|
+
Beta limits: live fresh-host/reboot acceptance, two-real-account channel switching and real exhausted-account/provider rejection remain pending. Fallback applies to scheduled agent tasks only; OpenCode/Pi/Grok advance only when their CLI is unavailable. The fallback command does not select named profiles, and different credentials do not establish independent quota allowances. Publication does not establish an installed-agent upgrade or authorize provider/domain writes.
|
|
11
|
+
|
|
12
|
+
## 0.1.0-beta.48
|
|
13
|
+
|
|
14
|
+
- Skip empty or unchanged scheduled work through owner-configured installed read-only plugin preflight. Invalid or unavailable reads fail closed; saved execution settings and recurring timing remain intact. The bound plugin broker is required.
|
|
15
|
+
- Run explicitly registered workspace scripts through the existing native scheduler without starting a model. Preserve registration revision/hash, cancellation, timeout, isolated environment and run receipts.
|
|
16
|
+
- Run Claude Code with an agent-bound configuration and native model/effort discovery. Older Claude sessions need `/new`; host login sharing supports OAuth only, and isolated agents log in separately.
|
|
17
|
+
- Keep owner channels responsive with configurable scheduled concurrency, default six.
|
|
18
|
+
- Expose bounded, sanitized owner run and delivery evidence through native controls.
|
|
19
|
+
- Repair transient plugin admission and isolated broker readiness after atomic binding changes; include isolated executor utilities.
|
|
20
|
+
- Keep Codex version discovery JSON separate from stderr notices.
|
|
21
|
+
- Validate Docker CI against audited Codex 0.160.1 instead of a moving registry tag; managed native version selection is unchanged.
|
|
22
|
+
- Read scoped TXT/PDF and JPEG/PNG/WebP attachments through the shared native MCP path. MP4 attachments supply bounded sampled frames to native vision; speech remains in the provider-sourced incoming transcript.
|
|
23
|
+
|
|
24
|
+
- Add short-lived browser conversation links for explicitly granted restricted discussions, retaining each task’s context and tool permissions.
|
|
25
|
+
- Preserve the existing runtime file owner when an administrator replaces discussion application grants, keeping private mode 0600 and preventing restricted task startup failures.
|
|
26
|
+
- Forward only the existing broker and delivery socket paths to the trusted task MCP subprocess, allowing scoped browser launch through the isolated installed launcher without exposing a tool registry.
|
|
27
|
+
- Audit restricted native tools on Codex 0.160.1 and retain the exact version gate.
|
|
28
|
+
- Extend the existing speech renderer with OpenRouter TTS and Ogg Opus/WAV encoding.
|
|
29
|
+
- Allow authorized task replies to use core-rendered voice on supporting channels, with post-synthesis authorization and unchanged uncertain-send protection.
|
|
30
|
+
|
|
31
|
+
Beta limits: live fresh-host/reboot acceptance remains pending. Claude host OAuth login sharing and human voice/media exchanges retain their documented live-QA limitations. Publication does not establish an installed-agent upgrade or activate new schedule gates.
|
|
32
|
+
|
|
3
33
|
## 0.1.0-beta.47
|
|
4
34
|
|
|
5
35
|
- Trigger an existing scheduled task manually with its saved instructions, model,
|
package/Dockerfile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
FROM docker/compose-bin:v5.5.1@sha256:14162382692aae43977f79effa64a499d61de19d4645461881daaf88773f7e74 AS compose-bin
|
|
2
2
|
FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca6d3ea96f16cb30df6187d94 AS dependencies
|
|
3
3
|
WORKDIR /app
|
|
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/*
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg poppler-utils docker.io jq python3 python-is-python3 && rm -rf /var/lib/apt/lists/*
|
|
5
5
|
COPY --from=compose-bin /docker-compose /usr/local/bin/docker-compose
|
|
6
6
|
ENV PNPM_HOME=/opt/pnpm COREPACK_HOME=/opt/corepack COREPACK_DEFAULT_TO_LATEST=0
|
|
7
7
|
ENV PATH=/opt/pnpm:$PATH
|
|
@@ -30,6 +30,10 @@ RUN pnpm add --global --allow-build=opencode-ai@${OPENCODE_CLI_VERSION} opencode
|
|
|
30
30
|
# Pin the reviewed CLI for repeatable release images.
|
|
31
31
|
ARG PI_CLI_VERSION=0.87.1
|
|
32
32
|
RUN pnpm add --global --ignore-scripts @earendil-works/pi-coding-agent@${PI_CLI_VERSION} && command -v pi
|
|
33
|
+
# Isolated agents select Claude Code from the relay menu with an agent-bound
|
|
34
|
+
# CLAUDE_CONFIG_DIR login (control/cli/claude). Pin the reviewed CLI.
|
|
35
|
+
ARG CLAUDE_CLI_VERSION=2.1.286
|
|
36
|
+
RUN pnpm add --global --allow-build=@anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} && command -v claude && claude --version
|
|
33
37
|
RUN chmod +x docker/entrypoint.sh bin/ez bin/ezenciel-agents* && mkdir -p /state/control /state/home /workspace && chown node:node /state/control /state/home /workspace
|
|
34
38
|
RUN node -e 'for (const [name, target] of Object.entries(require("./package.json").bin)) require("node:fs").symlinkSync("/app/" + target, "/usr/local/bin/" + name); require("node:fs").symlinkSync("/app/bin/ez", "/usr/local/bin/ez")'
|
|
35
39
|
# Build identity for relay status (package version stays release-owned).
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -8,9 +8,12 @@ The container is built locally; no bundled third-party binary image is promised.
|
|
|
8
8
|
|
|
9
9
|
Direct runtime dependencies: grammy (MIT), tsx (MIT). Development dependencies
|
|
10
10
|
include TypeScript (Apache-2.0) and Node type definitions (MIT). The runtime
|
|
11
|
-
image installs ffmpeg, Debian packages, and these reviewed CLI versions:
|
|
12
|
-
`@openai/codex`
|
|
13
|
-
`@earendil-works/pi-coding-agent` 0.87.1 (MIT)
|
|
11
|
+
image installs ffmpeg, Poppler (`poppler-utils`, GPL-2.0-or-later), Debian packages, and these reviewed CLI versions:
|
|
12
|
+
`@openai/codex` (Apache-2.0; the build resolves and records the selected version),
|
|
13
|
+
`opencode-ai` 1.18.32 (MIT), `@earendil-works/pi-coding-agent` 0.87.1 (MIT), and
|
|
14
|
+
`@anthropic-ai/claude-code` 2.1.286 (proprietary; Anthropic's legal agreements,
|
|
15
|
+
as referenced by its installed LICENSE.md, govern use). Preserve its installed
|
|
16
|
+
license and README; it is not covered by this repository's MIT license. Redistributing the image
|
|
14
17
|
requires review of its exact package build, notices and corresponding-source
|
|
15
18
|
obligations. Docker and
|
|
16
19
|
host-capable AI CLIs remain separately installed products, not covered by this
|
package/bin/ez
CHANGED
|
@@ -91,6 +91,13 @@ const main = async () => {
|
|
|
91
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');
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
+
if(args[0]==='--task-application') {
|
|
95
|
+
if(args.length!==1 || !runId || !RUN_ID.test(runId))throw Error('Active task run required')
|
|
96
|
+
const result=await request({version:1,id:randomUUID(),operation:'task-launch',runId,stdin:await readStdin()})
|
|
97
|
+
if(!result?.ok)throw Error(result?.error || 'Task launch failed')
|
|
98
|
+
if(result.stdout)process.stdout.write(result.stdout)
|
|
99
|
+
process.exitCode=result.code;return
|
|
100
|
+
}
|
|
94
101
|
const stdin = ['plugins', 'tools', 'updates', 'status'].includes(args[0]) ? '' : await readStdin();
|
|
95
102
|
if (['plugins', 'tools', 'updates', 'status'].includes(args[0])) await manager(args);
|
|
96
103
|
else await invoke(args, stdin);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Trusted operator diagnostic, executed inside the installed broker. Never a
|
|
2
|
+
// socket operation or native-run substitute; no command execution or writes.
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { loadPluginBrokerBinding } from '/app/src/plugin-broker.mjs';
|
|
7
|
+
import { registry, snapshot, checkFolders, hostNetworkBindings } from '/app/src/plugins/manager.mjs';
|
|
8
|
+
|
|
9
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export async function brokerReadiness(input, expected) {
|
|
11
|
+
if (!Array.isArray(expected) || expected.length !== 3 || expected.some(value => !/^[a-f0-9]{64}$/.test(value)))
|
|
12
|
+
throw Error('Expected host, registry and configuration fingerprints');
|
|
13
|
+
const binding = await loadPluginBrokerBinding(input);
|
|
14
|
+
const files = [binding.hostConfig, path.join(binding.home, 'registry.json'), path.join(binding.home, 'config.json')];
|
|
15
|
+
const verify = async () => {
|
|
16
|
+
for (let i = 0; i < files.length; i++) if (hash(await fs.readFile(files[i])) !== expected[i])
|
|
17
|
+
throw Error('Isolated broker configuration fingerprint mismatch');
|
|
18
|
+
};
|
|
19
|
+
await verify();
|
|
20
|
+
if (!(await fs.stat(binding.socket)).isSocket()) throw Error('Isolated broker socket unavailable');
|
|
21
|
+
const config = JSON.parse(await fs.readFile(files[2], 'utf8'));
|
|
22
|
+
const r = await registry(binding.home), plugins = [];
|
|
23
|
+
for (const [id, record] of Object.entries(r.plugins)) {
|
|
24
|
+
const trusted = await snapshot(record.source);
|
|
25
|
+
if (trusted.manifest.id !== id || record.source !== trusted.source || record.revision !== trusted.revision ||
|
|
26
|
+
JSON.stringify(record.manifest) !== JSON.stringify(trusted.manifest) ||
|
|
27
|
+
JSON.stringify(record.deployment) !== JSON.stringify(trusted.deployment) ||
|
|
28
|
+
JSON.stringify(record.sharedRevisions || {}) !== JSON.stringify(trusted.sharedRevisions))
|
|
29
|
+
throw Error('Registry plugin identity is not the reviewed source');
|
|
30
|
+
for (const alias of Object.keys(record.manifest.commands))
|
|
31
|
+
if (r.commands[alias] !== id || !record.deployment.commands[alias]) throw Error('Corrupt command registry');
|
|
32
|
+
await checkFolders(config, record, binding.home);
|
|
33
|
+
await hostNetworkBindings(config, record, binding.home);
|
|
34
|
+
plugins.push({ id, revision: record.revision });
|
|
35
|
+
}
|
|
36
|
+
await verify();
|
|
37
|
+
return { version: 1, ready: true, hostSha256: expected[0], registrySha256: expected[1], configSha256: expected[2], plugins };
|
|
38
|
+
}
|
|
39
|
+
{
|
|
40
|
+
const args = process.argv.slice(process.argv[1]?.endsWith('/broker-readiness.mjs') ? 2 : 1);
|
|
41
|
+
try {
|
|
42
|
+
const result = await brokerReadiness({
|
|
43
|
+
home: process.env.EZ_PLUGIN_BROKER_HOME, workspace: process.env.EZ_PLUGIN_BROKER_WORKSPACE,
|
|
44
|
+
controlDir: process.env.EZ_PLUGIN_BROKER_CONTROL_DIR, socket: process.env.EZ_PLUGIN_BROKER_SOCKET,
|
|
45
|
+
hostConfig: process.env.EZ_PLUGIN_BROKER_HOST_CONFIG,
|
|
46
|
+
}, args);
|
|
47
|
+
console.log(JSON.stringify(result));
|
|
48
|
+
} catch {
|
|
49
|
+
// Validation exceptions can contain private paths. Keep operator/public
|
|
50
|
+
// readback content-free; updater still retains failure and rollback state.
|
|
51
|
+
console.error('EZ_BROKER_STRUCTURAL_READINESS_FAILED');
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Explicit synthetic Linux mount regression. No credentials, native identities,
|
|
2
|
+
// provider calls or existing deployments. Run against the exact reviewed image:
|
|
3
|
+
// EZ_RELAY_IMAGE=<immutable image ID> node docker/broker-update-smoke.mjs
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { atomic, snapshot, publishBrokerPlugins } from '../src/plugins/manager.mjs';
|
|
10
|
+
import { execute, refreshBroker, verifyBroker, brokerNeedsRefresh } from '../src/updates/runtime.mjs';
|
|
11
|
+
const docker=(...args)=>execFileSync('docker',args,{encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout:120000}).trim();
|
|
12
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
if(process.argv[2]!=='--inside') {
|
|
14
|
+
const image=process.env.EZ_RELAY_IMAGE;
|
|
15
|
+
if(!/^sha256:[a-f0-9]{64}$/.test(image||''))throw Error('Supply the exact reviewed runtime image ID');
|
|
16
|
+
const volume=`e245-${process.pid}`;
|
|
17
|
+
try {
|
|
18
|
+
docker('volume','create',volume);
|
|
19
|
+
const root=JSON.parse(docker('volume','inspect',volume))[0].Mountpoint;
|
|
20
|
+
const output=docker('run','--rm','--network','none','--mount',`type=volume,source=${volume},target=${root}`,
|
|
21
|
+
'--mount','type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock',
|
|
22
|
+
'--env','EZ_DOCKER_COMPOSE=standalone','--entrypoint','node',image,'--import','/app/node_modules/tsx/dist/loader.mjs',
|
|
23
|
+
'/app/docker/broker-update-smoke.mjs','--inside',root,image);
|
|
24
|
+
console.log(output);
|
|
25
|
+
}finally {docker('volume','rm',volume);}
|
|
26
|
+
} else {
|
|
27
|
+
const root=process.argv[3],image=process.argv[4],home=path.join(root,'tools'),workspace=path.join(root,'mind'),controlDir=path.join(root,'control');
|
|
28
|
+
const hostConfig=path.join(root,'host-executor.json'),socket=path.join(controlDir,'b.sock'),project=`e245-${hash(root).slice(0,12)}`;
|
|
29
|
+
const config={schemaVersion:1,workspace,hostConfig,deploymentDir:root};
|
|
30
|
+
const args=['compose','--env-file',path.join(root,'docker.env')];
|
|
31
|
+
const call=(tail)=>execute('docker',[...args,...tail],{stdoutOnly:true,timeout:120000});
|
|
32
|
+
const container=async name=>(await call(['ps','-q',name])).trim();
|
|
33
|
+
const inside=async script=>call(['exec','-T','plugin-broker','node','--input-type=module','-e',script]);
|
|
34
|
+
const prepare=()=>inside(`import {prepareCommand} from '/app/src/plugins/manager.mjs'; await prepareCommand(${JSON.stringify(home)},'sample',[]); console.log('prepared');`);
|
|
35
|
+
const mounted=()=>inside(`import fs from 'node:fs'; import {createHash} from 'node:crypto'; console.log(createHash('sha256').update(fs.readFileSync(${JSON.stringify(hostConfig)})).digest('hex'));`);
|
|
36
|
+
const fingerprints=async()=>Promise.all([hostConfig,path.join(home,'registry.json'),path.join(home,'config.json')].map(async f=>hash(await fs.readFile(f))));
|
|
37
|
+
const probe=async()=>call(['exec','-T','plugin-broker','node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/docker/broker-readiness.mjs',...await fingerprints()]);
|
|
38
|
+
const agent={name:'synthetic',toolsHome:home,workspace,controlDir};
|
|
39
|
+
const host=(revisions)=>({isolation:'isolated',agents:[{...agent,pluginNetworkBindings:{sample:{revisions,bindings:[{service:'sample',network:'synthetic-reviewed-network'}]}}}]});
|
|
40
|
+
const records=[];
|
|
41
|
+
for(const directory of [home,workspace,controlDir])await fs.mkdir(directory,{recursive:true});
|
|
42
|
+
for(const version of ['1.0.0','1.0.1']) {
|
|
43
|
+
const source=path.join(home,'packages','sample',version);
|
|
44
|
+
await fs.mkdir(path.join(source,'bin'),{recursive:true});
|
|
45
|
+
const manifest={schemaVersion:1,id:'sample',version,commands:{sample:{executable:'bin/example.mjs',args:[]}},skills:[]};
|
|
46
|
+
const deployment={schemaVersion:1,services:{sample:{buildTarget:'runtime',volumes:{},healthcheck:['node','--version']}},commands:{sample:{service:'sample',argv:['node','/app/bin/example.mjs']}},exports:{}};
|
|
47
|
+
for(const [name,body] of Object.entries({'package.json':JSON.stringify({name:'synthetic-plugin',version,files:['bin']}),'ez-plugin.json':JSON.stringify(manifest),'ez-deployment.json':JSON.stringify(deployment),'Dockerfile':'FROM scratch AS runtime','.dockerignore':'','bin/example.mjs':'console.log("synthetic")'}))await fs.writeFile(path.join(source,name),body);
|
|
48
|
+
const s=await snapshot(source);
|
|
49
|
+
records.push({source,revision:s.revision,manifest:s.manifest,deployment:s.deployment,sharedRevisions:s.sharedRevisions,project:`ezp-${hash(home).slice(0,16)}-sample`,compose:path.join(home,'packages','sample','compose.json')});
|
|
50
|
+
}
|
|
51
|
+
const [a,b]=records;
|
|
52
|
+
const publish=record=>atomic(path.join(home,'registry.json'),{schemaVersion:1,owner:home,plugins:{sample:record},commands:{sample:'sample'}});
|
|
53
|
+
await atomic(path.join(home,'config.json'),config);await atomic(hostConfig,host([a.revision]));await publish(a);
|
|
54
|
+
const brokerEnv={EZ_PLUGIN_BROKER_HOME:home,EZ_PLUGIN_BROKER_WORKSPACE:workspace,EZ_PLUGIN_BROKER_CONTROL_DIR:controlDir,EZ_PLUGIN_BROKER_HOST_CONFIG:hostConfig,EZ_PLUGIN_BROKER_SOCKET:socket,EZ_DOCKER_COMPOSE:'standalone'};
|
|
55
|
+
const sleep=['node','-e','setInterval(()=>{},1000)'];
|
|
56
|
+
const compose={services:{relay:{image,network_mode:'none',entrypoint:sleep},'plugin-broker':{image,network_mode:'none',entrypoint:['node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/src/plugin-broker.mjs'],environment:brokerEnv,volumes:[`${home}:${home}`,`${workspace}:${workspace}:ro`,`${controlDir}:${controlDir}`,`${hostConfig}:${hostConfig}:ro`,'/var/run/docker.sock:/var/run/docker.sock'],healthcheck:{test:['CMD','node','-e',`process.exit(require('fs').statSync(${JSON.stringify(socket)}).isSocket()?0:1)`],interval:'1s',timeout:'5s',retries:15}}}};
|
|
57
|
+
await atomic(path.join(root,'compose.json'),compose);
|
|
58
|
+
await fs.writeFile(path.join(root,'docker.env'),`COMPOSE_PROJECT_NAME=${project}\nCOMPOSE_FILE=${path.join(root,'compose.json')}\nEZ_EXECUTOR_TRANSPORT=local\n`);
|
|
59
|
+
try {
|
|
60
|
+
await call(['up','-d','--no-build','--pull','never','--wait','--wait-timeout','30']);
|
|
61
|
+
const relay=await container('relay'),oldBroker=await container('plugin-broker'),oldHash=(await mounted()).trim();
|
|
62
|
+
assert.equal(JSON.parse(await probe()).ready,true);assert.equal((await prepare()).trim(),'prepared');
|
|
63
|
+
await atomic(hostConfig,host([a.revision,b.revision]));await publish(b);
|
|
64
|
+
assert.equal((await mounted()).trim(),oldHash);assert.notEqual(oldHash,(await fingerprints())[0]);
|
|
65
|
+
await assert.rejects(prepare(),/not pinned/);await assert.rejects(probe(),/STRUCTURAL_READINESS_FAILED/);
|
|
66
|
+
console.log('PASS stale inode: live socket; mounted A; registry B rejected; structural probe rejected');
|
|
67
|
+
const ready=await refreshBroker(config);
|
|
68
|
+
const newBroker=await container('plugin-broker');assert.notEqual(newBroker,oldBroker);assert.equal(await container('relay'),relay);
|
|
69
|
+
assert.equal((await mounted()).trim(),ready.hostSha256);assert.equal((await prepare()).trim(),'prepared');
|
|
70
|
+
assert.equal(ready.plugins[0].revision,b.revision);
|
|
71
|
+
console.log(JSON.stringify({stage:'activation',oldBroker,newBroker,relay,hostSha256:ready.hostSha256,registrySha256:ready.registrySha256,revision:b.revision}));
|
|
72
|
+
// With the current host binding already approving both revisions, plugin-only
|
|
73
|
+
// activation/rollback must leave the broker socket and relay intact.
|
|
74
|
+
await publish(a);await publishBrokerPlugins(home,controlDir);
|
|
75
|
+
const pluginRollback=await verifyBroker(config);
|
|
76
|
+
assert.equal(pluginRollback.plugins[0].revision,a.revision);
|
|
77
|
+
assert.equal(await container('plugin-broker'),newBroker);assert.equal(await container('relay'),relay);
|
|
78
|
+
assert.equal(await brokerNeedsRefresh(config),false);assert.equal((await prepare()).trim(),'prepared');
|
|
79
|
+
await publish(b);await publishBrokerPlugins(home,controlDir);
|
|
80
|
+
const pluginActivation=await verifyBroker(config);
|
|
81
|
+
assert.equal(pluginActivation.plugins[0].revision,b.revision);
|
|
82
|
+
assert.equal(await container('plugin-broker'),newBroker);assert.equal(await container('relay'),relay);
|
|
83
|
+
assert.equal((await prepare()).trim(),'prepared');
|
|
84
|
+
console.log('PASS plugin activation/rollback: current registry accepted; broker and relay IDs preserved');
|
|
85
|
+
|
|
86
|
+
await atomic(hostConfig,host([a.revision]));
|
|
87
|
+
await assert.rejects(refreshBroker(config),/STRUCTURAL_READINESS_FAILED/);await assert.rejects(prepare(),/not pinned/);
|
|
88
|
+
console.log('PASS unapproved B remains rejected after recreation');
|
|
89
|
+
await publish(a);const rolledBack=await refreshBroker(config);
|
|
90
|
+
assert.equal((await mounted()).trim(),rolledBack.hostSha256);assert.equal(rolledBack.plugins[0].revision,a.revision);assert.equal((await prepare()).trim(),'prepared');assert.equal(await container('relay'),relay);
|
|
91
|
+
console.log(JSON.stringify({stage:'rollback',broker:await container('plugin-broker'),relay,hostSha256:rolledBack.hostSha256,registrySha256:rolledBack.registrySha256,revision:a.revision}));
|
|
92
|
+
// Read-only probe must neither repair a tampered snapshot nor create leases.
|
|
93
|
+
await fs.appendFile(path.join(a.source,'bin/example.mjs'),'\n// unreviewed');await assert.rejects(probe(),/STRUCTURAL_READINESS_FAILED/);
|
|
94
|
+
await fs.writeFile(path.join(a.source,'bin/example.mjs'),'console.log("synthetic")');
|
|
95
|
+
await assert.rejects(call(['exec','-T','--env',`EZ_PLUGIN_BROKER_HOST_CONFIG=${root}/missing.json`,'plugin-broker','node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/docker/broker-readiness.mjs',...await fingerprints()]),/STRUCTURAL_READINESS_FAILED/);
|
|
96
|
+
await fs.rm(hostConfig);await assert.rejects(probe(),{code:'ENOENT'});
|
|
97
|
+
await atomic(hostConfig,host([a.revision]));await refreshBroker(config);
|
|
98
|
+
console.log('PASS source tamper and missing host file fail; no provider command or native run identity used');
|
|
99
|
+
}finally {await call(['down','--volumes','--remove-orphans']);}
|
|
100
|
+
}
|
package/docker/run.ts
CHANGED
|
@@ -20,7 +20,7 @@ try { privateEnv = parseEnv(readFileSync(3, 'utf8')) } catch (error) {
|
|
|
20
20
|
if ((error as NodeJS.ErrnoException).code !== 'EINVAL' && (error as NodeJS.ErrnoException).code !== 'EBADF') throw error
|
|
21
21
|
} finally { try { closeSync(3) } catch {} }
|
|
22
22
|
for (const [key, value] of Object.entries(privateEnv)) {
|
|
23
|
-
if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN'].includes(key)) process.env[key] = value
|
|
23
|
+
if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'EZ_SPEECH_PROVIDER', 'EZ_SPEECH_MODEL', 'EZ_SPEECH_VOICE_EN', 'EZ_SPEECH_VOICE_ES', 'EZ_CHANNEL_BACKEND_TOKEN', 'EZ_SCHEDULED_CONCURRENCY'].includes(key)) process.env[key] = value
|
|
24
24
|
}
|
|
25
25
|
privateEnv = {}
|
|
26
26
|
const [command = 'start', ...args] = process.argv.slice(2)
|
package/docker/upgrade-smoke.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const exec=promisify(execFile),root=await fs.realpath(await fs.mkdtemp(path.join
|
|
|
14
14
|
const source=path.join(root,'source'),home=path.join(root,'tools'),mind=path.join(root,'mind'),control=path.join(root,'control');
|
|
15
15
|
const input=path.resolve(process.env.EZ_WHATSAPP_SOURCE||new URL('../../ez_whatsapp',import.meta.url).pathname);
|
|
16
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);
|
|
17
|
+
const call=async(...args)=>JSON.parse((await exec(process.execPath,[bin,'--home',home,...args],{cwd:mind,maxBuffer:4*1024*1024})).stdout);
|
|
18
18
|
let record;
|
|
19
19
|
try {
|
|
20
20
|
for(const dir of [source,mind,control])await fs.mkdir(dir);
|
|
@@ -20,7 +20,7 @@ unfinished run returns 409, missing speech configuration 503, and provider
|
|
|
20
20
|
failure 502. Depleted Gemini credits return 503 with
|
|
21
21
|
`code: speech_credits_depleted`. Audio responses have `Cache-Control: no-store`.
|
|
22
22
|
|
|
23
|
-
Configure `GEMINI_API_KEY` on the relay. The existing speech renderer uses
|
|
23
|
+
Configure `GEMINI_API_KEY` on the relay. Alternatively select `EZ_SPEECH_PROVIDER=openrouter`, supply `OPENROUTER_API_KEY` privately on the relay, and set `EZ_SPEECH_MODEL` (default `fish-audio/s2.1-pro-free:free`). The same renderer converts provider MP3 to the channel’s WAV or Ogg Opus format; provider failures do not select another provider. The existing speech renderer uses
|
|
24
24
|
`gemini-3.8-flash-lite-tts`; Telegram still receives Ogg Opus. Applications should
|
|
25
25
|
send `{"language":"en"}` or `{"language":"es"}` to select
|
|
26
26
|
`EZ_SPEECH_VOICE_EN` or `EZ_SPEECH_VOICE_ES` (both default to Kore). Telegram
|
|
@@ -451,3 +451,34 @@ outgoing file downloads remain unsupported by this HTTP channel.
|
|
|
451
451
|
|
|
452
452
|
Private scope model selections are stored on that scope’s native-session binding.
|
|
453
453
|
They do not add entries to the owner’s saved AI menu or consume its preset limit.
|
|
454
|
+
|
|
455
|
+
## Restricted discussion browser handoff
|
|
456
|
+
|
|
457
|
+
A trusted application may host a browser conversation for existing exact-contact
|
|
458
|
+
tasks without inheriting the owner’s workspace or general tools. Outside native
|
|
459
|
+
turns, the installing administrator explicitly grants an installed launch command:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
ezenciel-agents-application --id browser --task-launch-command voice \
|
|
463
|
+
--task-launch-args '["launch","--task"]' --task-launch-tasks '["task_0123456789abcdef0123456789abcdef"]'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Use `--task-launch-tasks '"all"'` only with owner approval for all active
|
|
467
|
+
exact-contact discussions. `--disable-task-launch` revokes this handoff while
|
|
468
|
+
keeping the application binding. Wildcard public tasks cannot launch links.
|
|
469
|
+
The restricted native model sees `browser_link` with the approved application
|
|
470
|
+
names; it cannot choose another task, command arguments or identity. The trusted
|
|
471
|
+
broker executes the installed command with a 20-minute task bearer through private
|
|
472
|
+
stdin. The model receives only the resulting short-lived HTTPS browser link.
|
|
473
|
+
This is an explicit mutating capability, separate from read-only channel queries.
|
|
474
|
+
|
|
475
|
+
The backend checks its task bearer with `POST /v1/registration` and
|
|
476
|
+
`{taskToken}`. It submits `{requestId,scope:"task:TASK_ID",text,taskToken}` to
|
|
477
|
+
`/v1/runs`. Core derives the exact task and contact, binds the original grant and
|
|
478
|
+
application, rechecks owner/source/account/expiry/revocation, and uses the same
|
|
479
|
+
restricted native task executor and task notes. Task requests cannot select
|
|
480
|
+
owner history, AI, files or native sessions. `send` replies to the application
|
|
481
|
+
inbox as text; it does not send an additional channel message. The task bearer
|
|
482
|
+
is hashed in core storage, stays in the trusted backend and expires no later
|
|
483
|
+
than the task. Changed retries and cross-task/application use fail closed.
|
|
484
|
+
No contributor becomes an installation owner.
|
|
@@ -12,7 +12,11 @@ Each named conversation saves its model and reasoning level. Switching back thro
|
|
|
12
12
|
`/ai` shows the current choice and saves changes immediately. Changing model or
|
|
13
13
|
reasoning level on the same client and provider preserves the conversation's name
|
|
14
14
|
and native history; subsequent turns pass the new settings to the native engine.
|
|
15
|
-
Changing client or
|
|
15
|
+
Changing client, provider or auth profile starts a separate native conversation.
|
|
16
|
+
A conversation records its client and auth profile when created; launch rejects a
|
|
17
|
+
choice for another profile, so a native session never resumes under another
|
|
18
|
+
account. Provisioned profiles appear as additional catalog entries for their
|
|
19
|
+
client (see [auth profiles](../docker-runtime.md#auth-profiles)). Old bindings
|
|
16
20
|
remain available so accepted work can finish and owners can reopen their chats.
|
|
17
21
|
|
|
18
22
|
Grok/Claude accept caller-selected native UUIDs. Codex/OpenCode generate IDs, so
|
|
@@ -31,6 +35,16 @@ cross-turn, command, file and unrelated requests are declined rather than being
|
|
|
31
35
|
guessed or forwarded to a hidden task UI.
|
|
32
36
|
Legacy unbound sessions require the owner's explicit `/new`.
|
|
33
37
|
|
|
38
|
+
Like Codex's `CODEX_HOME`, Claude runs with an agent-bound `CLAUDE_CONFIG_DIR`
|
|
39
|
+
(`control/cli/claude`): settings, memory, plugins and sessions belong to the
|
|
40
|
+
agent, never the operator's `~/.claude`. Host-capable agents share only the
|
|
41
|
+
installer's OAuth/subscription login by setting `CLAUDE_SECURESTORAGE_CONFIG_DIR`
|
|
42
|
+
empty, so Claude reads and refreshes its default credential store in place. API
|
|
43
|
+
key, `apiKeyHelper` or provider `env` setups in the operator's `~/.claude` are
|
|
44
|
+
not shared; put them in the agent-bound directory instead. Isolated agents log in
|
|
45
|
+
inside their own config directory. Claude conversations started before this
|
|
46
|
+
binding live in the operator's `~/.claude` and need `/new`.
|
|
47
|
+
|
|
34
48
|
References used for the adapter contract:
|
|
35
49
|
|
|
36
50
|
- [Codex non-interactive execution](https://learn.chatgpt.com/docs/non-interactive-mode)
|
|
@@ -42,7 +56,12 @@ environment and the execution-bound `CODEX_HOME`; discovery starts no session
|
|
|
42
56
|
or inference. Only visible model names and supported efforts are projected.
|
|
43
57
|
Unavailable discovery retains the native cache. The installed native version
|
|
44
58
|
still owns which models it advertises; Core never guesses newly released aliases.
|
|
45
|
-
Grok/Codex/OpenCode support explicit listed model/effort choices.
|
|
59
|
+
Grok/Codex/OpenCode/Claude support explicit listed model/effort choices. Claude has
|
|
60
|
+
no catalog command, so the installed `claude --help` is its native metadata: the
|
|
61
|
+
documented model aliases and `--effort` levels are listed alongside the client
|
|
62
|
+
default; unparseable help keeps the client default only. A versioned model name (for example
|
|
63
|
+
`claude-sonnet-5-5`) is offered by adding a `claude` entry to the agent's curated
|
|
64
|
+
`control/ai-models.json`; the alias `sonnet` always means the client's current Sonnet. Other installed clients
|
|
46
65
|
offer their own default only in this slice. OpenCode lists the installed `opencode models`
|
|
47
66
|
catalog with the whitelisted executor environment, so only models the relay can actually
|
|
48
67
|
run are offered; provider variants become selectable efforts (`--variant`).
|
|
@@ -64,8 +83,8 @@ Refresh available AIs repeats default discovery. Active/default
|
|
|
64
83
|
presets and queued snapshots are preserved; discovery only refreshes unused
|
|
65
84
|
detected entries.
|
|
66
85
|
Codex uses its native `config/read` interface; Grok reads its documented user
|
|
67
|
-
model/effort settings (or `models` for the default model). Claude reads
|
|
68
|
-
workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
|
|
86
|
+
model/effort settings (or `models` for the default model). Claude reads the agent-bound
|
|
87
|
+
user settings and workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
|
|
69
88
|
opaque wrappers remain explicitly “client default”. No credentials are stored,
|
|
70
89
|
no inference runs, no new dependency, and no cross-CLI session transfer.
|
|
71
90
|
|
package/docs/docker-runtime.md
CHANGED
|
@@ -272,6 +272,57 @@ workspace (a tenant farm), while allowing explicitly shared and additional
|
|
|
272
272
|
workspace grants and leaving `$HOME` readable for the host CLI.
|
|
273
273
|
Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
|
|
274
274
|
|
|
275
|
+
## Claude Code auth
|
|
276
|
+
|
|
277
|
+
Isolated agents run the image Claude Code with `CLAUDE_CONFIG_DIR` set to the
|
|
278
|
+
agent-bound `control/cli/claude`; settings, memory, plugins and sessions stay
|
|
279
|
+
there. Log the agent in once inside its relay, which writes its own
|
|
280
|
+
`.credentials.json` there and never shares or copies the operator login:
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
docker compose exec -it -u "${EZ_RUNTIME_UID:-1000}:${EZ_RUNTIME_GID:-1000}" relay \
|
|
284
|
+
sh -c 'CLAUDE_CONFIG_DIR="$EZ_CONTROL_DIR/cli/claude" claude auth login'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
To share one login across many agents without logging in one by one, create a
|
|
288
|
+
long-lived token once with `claude setup-token` and copy it to each agent's
|
|
289
|
+
`control/cli/claude/oauth-token` (mode `600`). It does not refresh, so agents
|
|
290
|
+
cannot invalidate each other's session the way copied `.credentials.json`
|
|
291
|
+
files can. The file's contents reach only the Claude process as
|
|
292
|
+
`CLAUDE_CODE_OAUTH_TOKEN`; a malformed file fails the run closed.
|
|
293
|
+
|
|
294
|
+
Without any login, Claude turns fail closed with the native "not logged in"
|
|
295
|
+
error. Host-capable agents use the same agent-bound directory but share the
|
|
296
|
+
installer login in place (`CLAUDE_SECURESTORAGE_CONFIG_DIR` empty).
|
|
297
|
+
|
|
298
|
+
## Auth profiles
|
|
299
|
+
|
|
300
|
+
One agent can keep several owner-provisioned Codex or Claude logins, for example
|
|
301
|
+
a work and a personal account. Each named profile is a separate native home,
|
|
302
|
+
`control/cli/profiles/<name>/<cli>` (`CODEX_HOME` or `CLAUDE_CONFIG_DIR`), with
|
|
303
|
+
its own credentials, configuration and native sessions. The unnamed default
|
|
304
|
+
keeps the bindings above unchanged. Create the empty home and get the exact
|
|
305
|
+
native login command with:
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
ezenciel-agents-ai profile add --cli claude --auth-profile work
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Then the owner logs in once with the printed command, inside the relay for
|
|
312
|
+
isolated agents. Claude may instead use a `claude setup-token` token in that
|
|
313
|
+
home's `oauth-token` (mode `600`). Ez never reads, copies or prints the
|
|
314
|
+
credential. A named profile never links the host login or another home: an
|
|
315
|
+
unprovisioned or logged-out profile fails closed with the native error.
|
|
316
|
+
`ezenciel-agents-ai profiles` reports each home's login status (logged in,
|
|
317
|
+
method, credential file names) from the native CLI without tokens or account
|
|
318
|
+
identifiers.
|
|
319
|
+
|
|
320
|
+
Select a profile with `ezenciel-agents-ai select --cli claude --auth-profile work`,
|
|
321
|
+
the profile entries in Choose AI, or `ezenciel-agents-schedule create ...
|
|
322
|
+
--auth-profile work`. A conversation is bound to its CLI and profile; changing
|
|
323
|
+
profile starts a new native conversation, and a run whose request names another
|
|
324
|
+
profile than its captured choice is rejected before launch.
|
|
325
|
+
|
|
275
326
|
## OpenCode auth
|
|
276
327
|
|
|
277
328
|
Isolated agents run the image OpenCode with `XDG_DATA_HOME` set to the
|
|
@@ -319,3 +370,9 @@ restart after repair. Existing authorized work and outbox draining remain active
|
|
|
319
370
|
polling health remains unhealthy. Transient HTTP, rate-limit and server errors
|
|
320
371
|
may reconnect. grammY still owns reconnect behavior inside its poller; ez does
|
|
321
372
|
not override private library methods or implement a replacement poller.
|
|
373
|
+
|
|
374
|
+
The isolated executor image includes Node, jq and Python 3 (`python` and
|
|
375
|
+
`python3`) for task-local JSON/text processing. Domain calculations and writes
|
|
376
|
+
remain behind installed plugins. Registered command admission waits up to five
|
|
377
|
+
seconds for the short registry lock; lifecycle changes still fail closed, and
|
|
378
|
+
no caller removes an existing lock. Serialize calls when a plugin requires it.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Isolated plugin update readiness
|
|
2
|
+
Status: proposed acceptance contract; not a claim of installed behavior.
|
|
3
|
+
Source baseline: f192d57d8b1ea2748fe31544ffdb968c827b891e.
|
|
4
|
+
Scope: one causal repair to isolated broker activation and verification.
|
|
5
|
+
|
|
6
|
+
## Objective and boundaries
|
|
7
|
+
An authorized plugin update must leave the isolated native execution path able to resolve and prepare the exact reviewed plugin revision, or report failure with the existing recovery path. A successful host-side command, running container or socket is insufficient.
|
|
8
|
+
|
|
9
|
+
Constraints:
|
|
10
|
+
1. Preserve reviewed-revision, host-network, folder, owner and per-run authorization checks. Never authorize a revision merely to make a probe pass.
|
|
11
|
+
2. Keep one registry, broker and updater; no new daemon, retry controller, scheduler or financial workflow.
|
|
12
|
+
3. Preserve the relay, native sessions, unrelated plugins and persistent state during plugin-only refresh.
|
|
13
|
+
4. No credentials, customer data or production run identities in tests or public receipts.
|
|
14
|
+
5. Existing update serialization, active-work admission and uncertain-write reconciliation remain binding.
|
|
15
|
+
|
|
16
|
+
Owner: updater/broker boundary, principally src/updates/runtime.mjs and existing plugin validation. This does not change domain procedures or models.
|
|
17
|
+
|
|
18
|
+
## Observed mechanism and confidence
|
|
19
|
+
compose.yaml bind-mounts the host configuration as a single read-only file into plugin-broker. Configuration writers use temporary-file plus rename. A single-file bind mount can retain the prior inode after host-side atomic replacement. The plugin registry can therefore select the new reviewed revision while the broker still sees the old revision list. hostNetworkBindings in src/plugins/manager.mjs correctly rejects that mismatch.
|
|
20
|
+
|
|
21
|
+
refreshBroker currently invokes Compose restart after plugin registry activation and rollback. That does not explicitly recreate the container/mount. The current broker health check only verifies socket existence. The existing updater regression test confirms restart and inventory version, not mounted configuration identity or executable readiness.
|
|
22
|
+
|
|
23
|
+
Production diagnosis observed different host and mounted revision authorization, and targeted broker recreation restored the failing boundary. This supports the stale-mount mechanism; the exact historical writer/commit that first replaced the file is not yet established. Do not invent that provenance or attribute every domain deferral to this incident.
|
|
24
|
+
|
|
25
|
+
## Smallest implementation
|
|
26
|
+
First reproduce the failure with a synthetic deployment. Prefer fixing lifecycle of the existing narrowly scoped mount rather than broadening the mount or making authorization mutable inside the broker.
|
|
27
|
+
|
|
28
|
+
1. After approved activation changes and registry publication, refresh the isolated broker using the deployment's existing Compose identity and exact installed image. Recreate only plugin-broker, without dependencies/build/pull or relay replacement, and wait for readiness. Use supported equivalent Compose flags rather than hardcoding another project or image.
|
|
29
|
+
2. Apply the same mount-refresh rule to rollback. Restore the old registry/config state owned by the existing transaction, then verify the old revision is again usable. Never restore provider journals or silently erase subsequent unrelated state.
|
|
30
|
+
3. Ensure every updater path that replaces a relevant host file and retains an unchanged broker image cannot skip mount refresh merely because Compose sees the same image/configuration. Include main-update activation/rollback where relevant; do not gratuitously recreate healthy relay processes.
|
|
31
|
+
4. Before completed, compare broker-visible configuration identity with the intended host configuration and verify installed registry/source revision consistency and existing host-network/folder validation. The check must execute inside the actual isolated broker environment, be read-only and bounded, and expose no configuration contents or secrets.
|
|
32
|
+
5. Separate structural readiness from real native invocation evidence. A structural probe must not fabricate EZ_RUN_ID, borrow another run, create permanent authorization, or invoke an arbitrary plugin command that might mutate. Prefer existing prepare/validation primitives with a narrowly scoped trusted diagnostic if no supported read-only probe exists. An actual harmless command through a genuine authorized native run is a separate acceptance step.
|
|
33
|
+
6. A mismatched pin, missing mounted file, failed recreate or failed verification must prevent completed. Use current failed/rollback/recovery-required states and preserve error evidence. Do not swallow probe failures.
|
|
34
|
+
7. Keep socket liveness distinguishable from functional readiness in existing status/diagnostics. Do not build an expensive provider poll into a high-frequency health check or restart endlessly. Document exactly what each level proves.
|
|
35
|
+
|
|
36
|
+
If code inspection finds a simpler existing supported mechanism meeting all these requirements, use it and explain the equivalence in the PR. Do not bypass pinning, rewrite authorized revisions, mount the entire private deployment directory, or switch execution transport.
|
|
37
|
+
|
|
38
|
+
## Regression evidence
|
|
39
|
+
Use synthetic, isolated state and real Docker bind-mount behavior where required:
|
|
40
|
+
- Start with approved revision A and a single-file read-only host binding.
|
|
41
|
+
- Atomically replace host configuration with independently approved revision B and activate registry B; reproduce old mounted bytes and rejection in the uncorrected harness.
|
|
42
|
+
- Apply corrected refresh; prove broker sees exact new file identity/content hash and prepares B successfully. Record container identity change and unchanged relay identity.
|
|
43
|
+
- Prove a still-unapproved B remains rejected. Recreate is not authorization.
|
|
44
|
+
- Cover rollback B to A, including mounted bytes, registry identity and readiness.
|
|
45
|
+
- Recreate failure and readback/probe failure must not return completed.
|
|
46
|
+
- A socket that exists while configuration is stale or invalid must not count as functional-ready.
|
|
47
|
+
- Host-capable updates should not start an isolated broker.
|
|
48
|
+
- Preserve existing invalid owner/run, canceled/revoked admission, folder/network mismatch and secret-isolation tests relevant to changed code.
|
|
49
|
+
- Respect active work through existing updater admission. Do not manufacture a second drain/queue mechanism.
|
|
50
|
+
|
|
51
|
+
Update the existing restart-only test to assert the meaningful behavior. Mocked Compose argument assertions alone cannot prove Linux bind-mount recovery. No test should require customer credentials or invoke a financial command.
|
|
52
|
+
|
|
53
|
+
## Verification and review
|
|
54
|
+
Run required repository checks on final source: pnpm verify, pnpm run release:check, git diff --check and applicable Docker/packed-artifact checks in docs/releasing.md. Identify test counts, skipped/unrun checks and exact commit/artifact hash honestly. Keep one focused PR and do not include unrelated media work.
|
|
55
|
+
|
|
56
|
+
The implementation author supplies the final diff, root-cause reproduction, before/after evidence and limits. The assigned reviewer personally checks architecture, correctness, negative cases and exact-head CI, then merges using expected-head protection. Changes after review require affected revalidation. This specification is authored by the reviewer; its authorship is disclosed, and implementation verification must not be described as independent review of the reviewer's own design.
|
|
57
|
+
|
|
58
|
+
## Direct VM acceptance after merge
|
|
59
|
+
Use the deployment's supported updater on the reviewed artifact. Before activation, record non-secret installed revision/image identities, native active-work state and baseline read-only access; preserve ongoing work and use the updater's normal drain. Never force a restart during an active call.
|
|
60
|
+
|
|
61
|
+
Verify exact installed commit/artifact and broker/relay identities, mounted configuration fingerprint, reviewed registry identity and structural readiness. Then inspect a genuine native invocation using the same bound launcher, with a harmless help/manifest read and its real run receipt. Do not trigger scores, leverage, trades or other investment workflows as a smoke test. If no authorized diagnostic invocation exists, retain native proof pending the next natural invocation rather than inventing identity.
|
|
62
|
+
|
|
63
|
+
Success: the corrected lifecycle demonstrably handles atomic binding replacement and rollback, and the installed isolated path accepts the already authorized revision without operator recreation. Source merge, service health and a host-side doctor alone are not success.
|
|
64
|
+
|
|
65
|
+
## Separate subsequent work
|
|
66
|
+
Outage-to-engineering acknowledgement, recovery/resumption ownership, large-file archive transfer and shared daily-ledger contention are separate causes. They remain follow-up investigations; do not add automatic domain replay, broaden stdin limits, change schedules or rewrite record ownership in this PR.
|
package/docs/releasing.md
CHANGED
|
@@ -17,6 +17,10 @@ separate release bot is not required.
|
|
|
17
17
|
3. Build both Docker targets from a clean checkout:
|
|
18
18
|
`docker build --target test -t ez-release-tests .` and
|
|
19
19
|
`docker build --target runtime -t ez-release-runtime .`.
|
|
20
|
+
Docker CI pins the reviewed Codex baseline (`TASK_CODEX_VERSION`) with the
|
|
21
|
+
existing `CODEX_CLI_VERSION` build argument; update that pin when the native
|
|
22
|
+
audit advances. Baseline CI does not validate a newly published native version.
|
|
23
|
+
Managed installs and upgrades keep their existing native version selection.
|
|
20
24
|
Main relay: `EZ_RELAY_IMAGE=ez-release-runtime node docker/smoke.mjs`.
|
|
21
25
|
The WhatsApp Compose overlay has no separate relay image; verify its
|
|
22
26
|
provider integration with the synthetic plugin smoke in `docs/plugins.md`.
|