@jc_stack/ez-agents 0.1.0-beta.32 → 0.1.0-beta.33
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/AGENTS.md +46 -31
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +29 -26
- package/docs/plugin-contributions.md +4 -1
- package/docs/upgrades.md +6 -4
- package/package.json +1 -1
- package/src/updates/control.mjs +11 -1
- package/src/updates/runtime.mjs +3 -1
- package/test/updates.test.mjs +24 -1
package/AGENTS.md
CHANGED
|
@@ -73,6 +73,35 @@ can be stated briefly and deferred. Reviewers distinguish those blockers from
|
|
|
73
73
|
optional follow-ups and do not hold a working increment for hypothetical polish.
|
|
74
74
|
Measure progress by usable outcomes and feedback, not code or test volume.
|
|
75
75
|
|
|
76
|
+
## Feasibility frame for non-trivial work
|
|
77
|
+
|
|
78
|
+
Before implementing a non-trivial coding, architecture or product change, state
|
|
79
|
+
the user-visible objective, no more than five task-relevant binding constraints,
|
|
80
|
+
the component that owns the failed boundary, the subtraction or simplification
|
|
81
|
+
option, the minimum evidence needed to prove the outcome, and the stop condition.
|
|
82
|
+
Do not restate the full prompt or add this ceremony to minor mechanical edits.
|
|
83
|
+
|
|
84
|
+
A result that violates a binding constraint is invalid even when tests pass or
|
|
85
|
+
the immediate symptom disappears. Check behavioral correctness, architecture and
|
|
86
|
+
ownership, real user-path evidence and authorized side effects separately. Before
|
|
87
|
+
completion, inspect whether the change added a runner, wrapper, state owner, queue,
|
|
88
|
+
retry loop, prompt layer, dependency or configuration format; retain it only when
|
|
89
|
+
the existing owner or contract cannot solve the demonstrated problem. Use one
|
|
90
|
+
bounded constraint review, then stop when sufficient evidence passes unless a
|
|
91
|
+
concrete material risk remains.
|
|
92
|
+
|
|
93
|
+
Frame that review as answers to these questions, not as an open-ended request to
|
|
94
|
+
find flaws:
|
|
95
|
+
|
|
96
|
+
1. Did the solution cross an architecture, ownership or authority boundary?
|
|
97
|
+
2. Did it bypass a binding constraint or preserve the symptom through a workaround?
|
|
98
|
+
3. Did it add machinery where deletion or a simpler existing contract would work?
|
|
99
|
+
4. Does the evidence prove the real user-visible outcome rather than only a proxy?
|
|
100
|
+
5. Is there one concrete unresolved risk severe enough to block completion?
|
|
101
|
+
|
|
102
|
+
If no answer identifies a material blocker, finish. Do not start another review
|
|
103
|
+
round merely to seek more criticism, tests or hypothetical edge cases.
|
|
104
|
+
|
|
76
105
|
## 3. Crash-Safe Atomic Disk State
|
|
77
106
|
- All persistent stores (`ControlStore`, `RunStore`, outbox queue) must be disk-backed JSON files.
|
|
78
107
|
- **Atomic write pattern:** Never write directly to a state file. Always write to a temporary file (`${target}.${process.pid}.tmp`) with mode `0o600`, then atomically `rename` it over the destination.
|
|
@@ -97,37 +126,23 @@ Measure progress by usable outcomes and feedback, not code or test volume.
|
|
|
97
126
|
- Unapproved group messages from other senders and unknown DMs fail silently unless an exact-contact or any-conversation owner-approved channel grant admits them. Both grant types use the restricted task runner. Paired-owner group text is discovery routed to the owner's private chat; it grants no group reply authority.
|
|
98
127
|
- Stopping work (`/stop`) must terminate the active worker PID immediately (`SIGTERM`, escalating to `SIGKILL` if unclosed after 3s).
|
|
99
128
|
|
|
100
|
-
## 8.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
## 8. Risk-based negative validation
|
|
130
|
+
|
|
131
|
+
Changes to authority, execution, routing, secret isolation or durable state must
|
|
132
|
+
validate the affected failure boundary, not rerun or recreate an unrelated fixed
|
|
133
|
+
checklist. Examples include proving that an unapproved sender spawns no process,
|
|
134
|
+
executor secrets remain absent, invalid IDs cannot traverse paths, ambiguous
|
|
135
|
+
external writes are not retried, and corrupt state fails safely. Add or change a
|
|
136
|
+
test only when it protects the changed contract or a demonstrated regression.
|
|
137
|
+
Existing relevant checks plus focused real-path evidence may be sufficient.
|
|
107
138
|
|
|
108
139
|
## Public contribution workflow
|
|
109
140
|
|
|
110
|
-
Read CONTRIBUTING.md before edits and docs/releasing.md before a release.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Never treat task completion as permission to merge or publish.
|
|
119
|
-
Own the complete engineering/release handoff in CONTRIBUTING.md. Once ready,
|
|
120
|
-
proactively request only missing merge/release authority, then ship and verify;
|
|
121
|
-
do not leave the maintainer to discover ready drafts or operate the release.
|
|
122
|
-
|
|
123
|
-
For every core or plugin PR you author, revise or review, apply CONTRIBUTING.md's
|
|
124
|
-
architecture check and the README's engine/application boundary. A symptom fix
|
|
125
|
-
and green tests do not justify a conflicting execution or context owner. Existing
|
|
126
|
-
legacy channel-backend code is a documented limitation, not a pattern for new
|
|
127
|
-
integrations; applications submit agent turns through the native Ez execution path.
|
|
128
|
-
|
|
129
|
-
A coding-task handoff must include the pushed commit and draft PR URL, checks,
|
|
130
|
-
independent-review status and remaining QA. If PR creation is blocked, state the
|
|
131
|
-
blocker and preserved commit. The merging agent owns the post-merge worktree
|
|
132
|
-
cleanup check and reports removal or the specific reason to retain it. Follow
|
|
133
|
-
CONTRIBUTING.md for squash-merge evidence, ignored files and active-use checks.
|
|
141
|
+
Read CONTRIBUTING.md before edits and docs/releasing.md before a release. It owns
|
|
142
|
+
the worktree, PR, review, merge, release and cleanup process; do not duplicate that
|
|
143
|
+
process here or in feature documentation. For every core or plugin change, apply
|
|
144
|
+
its architecture check and the README's engine/application boundary. A symptom
|
|
145
|
+
fix and green tests do not justify a conflicting execution or context owner.
|
|
146
|
+
Existing legacy channel-backend code is a documented limitation, not a pattern
|
|
147
|
+
for new integrations. Keep internal plans and private evidence outside this
|
|
148
|
+
repository.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.33
|
|
4
|
+
|
|
5
|
+
- Allow compatible plugin updates to add commands to existing services, registering
|
|
6
|
+
their aliases atomically while retaining collision checks and rejecting changes
|
|
7
|
+
to existing command routes or deployment authority.
|
|
8
|
+
- Simplify contribution guidance and bound independent constraint reviews without
|
|
9
|
+
changing the native engine, transport or plugin responsibility boundaries.
|
|
10
|
+
|
|
3
11
|
## 0.1.0-beta.32
|
|
4
12
|
|
|
5
13
|
- Add shared authenticated inbound attachment staging for application and
|
package/CONTRIBUTING.md
CHANGED
|
@@ -16,9 +16,11 @@ repository. Public docs describe shipped behavior and explicit limitations.
|
|
|
16
16
|
Packaging/runtime changes also need the Docker checks in docs/releasing.md.
|
|
17
17
|
5. Open a PR explaining the problem, resulting behavior, verification and limits.
|
|
18
18
|
Include a short sanitized reproduction. State which tests were not run.
|
|
19
|
-
6. Obtain independent review and required CI,
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
6. Obtain one bounded independent final-head review and required CI, resolve
|
|
20
|
+
material findings, then complete the authorized merge. An owner or maintainer
|
|
21
|
+
request for a product change or fix authorizes this normal PR/merge loop;
|
|
22
|
+
unsolicited external contributions still require maintainer merge authority.
|
|
23
|
+
No CLA, ticket requirement, custom commit format or additional approval committee.
|
|
22
24
|
|
|
23
25
|
You are responsible for understanding submitted code, including AI-generated
|
|
24
26
|
code, and having the right to contribute it under this repository's license.
|
|
@@ -79,10 +81,10 @@ do not disguise them as supported architecture or start a duplicate repair.
|
|
|
79
81
|
create one worktree/PR per repository and link dependencies and merge order.
|
|
80
82
|
Branch from an unmerged feature only when the dependency is intentional and
|
|
81
83
|
documented; do not quietly include it in an unrelated PR.
|
|
82
|
-
- Push the task branch and open a
|
|
83
|
-
|
|
84
|
-
work
|
|
85
|
-
|
|
84
|
+
- Push the task branch and open a PR when the coherent change is reviewable. Use
|
|
85
|
+
draft status only while implementation or a named gate remains incomplete; do
|
|
86
|
+
not leave completed mergeable work in a draft or only on a local branch.
|
|
87
|
+
Publication remains a separate authority boundary.
|
|
86
88
|
- Every handoff names the repository, worktree, branch, exact commit, PR URL,
|
|
87
89
|
checks, independent-review status and remaining QA with its next action. If
|
|
88
90
|
pushing or PR creation is blocked, report the blocker and preserved local
|
|
@@ -98,9 +100,10 @@ do not disguise them as supported architecture or start a duplicate repair.
|
|
|
98
100
|
- New substantive commits or conflict resolutions invalidate the affected review
|
|
99
101
|
and test evidence. Refresh against current main, review the resulting diff and
|
|
100
102
|
run the applicable checks. Do not bypass protected-branch requirements.
|
|
101
|
-
- Merge only after review, required CI
|
|
102
|
-
Publish only under the separate release
|
|
103
|
-
releases. Record which source commit and
|
|
103
|
+
- Merge only after review, required CI and applicable QA under the authority in
|
|
104
|
+
the contribution checklist above. Publish only under the separate release
|
|
105
|
+
process; commits and merges are not releases. Record which source commit and
|
|
106
|
+
artifact hash were tested.
|
|
104
107
|
- Keep the worktree while its PR or QA is open. After merge or explicit abandonment,
|
|
105
108
|
inspect it for uncommitted/untracked files and local-only commits. Remove only
|
|
106
109
|
the clean task worktree after valuable work is preserved; never force cleanup.
|
|
@@ -124,22 +127,22 @@ and prepare the release. Reuse valid final-commit evidence; repeat checks when
|
|
|
124
127
|
changes or failures invalidate it. Do not leave a ready feature silently in draft
|
|
125
128
|
or ask the maintainer to run commands, coordinate reviewers, or operate CI.
|
|
126
129
|
|
|
127
|
-
The maintainer
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
credential/2FA, failed gate that cannot be repaired in
|
|
141
|
-
change. Human attention belongs on product intent;
|
|
142
|
-
|
|
130
|
+
The owner or maintainer request authorizes the requested work and its normal
|
|
131
|
+
PR/merge steps; do not ask them to approve the same work again. Release remains
|
|
132
|
+
separate authority: a request to merge does not authorize publication or changing
|
|
133
|
+
a private package's visibility, while a request to release authorizes the merge
|
|
134
|
+
and publication needed for that release. Prepare and verify the reviewed commits,
|
|
135
|
+
artifacts and checksums before shipping. If release was not requested, stop after
|
|
136
|
+
the merged contribution loop and report any distinct deployment or publication
|
|
137
|
+
step without performing it.
|
|
138
|
+
|
|
139
|
+
When release is authorized, complete publication using docs/releasing.md, then
|
|
140
|
+
verify registry metadata, the downloaded artifact and any explicitly requested
|
|
141
|
+
installed-runtime QA. Deployment is separate and requires a direct rollout
|
|
142
|
+
request or the target's saved update policy. Report the outcome. Escalate only a
|
|
143
|
+
product decision, missing credential/2FA, failed gate that cannot be repaired in
|
|
144
|
+
scope, or material scope change. Human attention belongs on product intent; the
|
|
145
|
+
agent operates the technical workflow.
|
|
143
146
|
|
|
144
147
|
Example, substituting a unique task name and an absolute external directory:
|
|
145
148
|
|
|
@@ -28,7 +28,10 @@ Before the first PR, provide:
|
|
|
28
28
|
|
|
29
29
|
Copy CONTRIBUTING.md, SECURITY.md structure, PR template and docs/releasing.md
|
|
30
30
|
from a released Ez repository; adapt commands and security facts to your plugin.
|
|
31
|
-
|
|
31
|
+
Add a short AGENTS.md that states the plugin's ownership boundary and applies the
|
|
32
|
+
same compact feasibility frame and risk-based validation policy; do not copy
|
|
33
|
+
core-only Telegram, state or dependency invariants. Do not copy private QA,
|
|
34
|
+
company policy, proposed features or unsupported claims.
|
|
32
35
|
Public instructions must work without the maintainer's parent workspace. Use
|
|
33
36
|
reviewed local source and its inspected content hash; registry installation is
|
|
34
37
|
inert. Installation is complete only after account onboarding and verified use.
|
package/docs/upgrades.md
CHANGED
|
@@ -174,10 +174,12 @@ No backup deletes live data or revokes credentials. Retain old package roots and
|
|
|
174
174
|
images until QA and any recovery window are complete; there is no automatic GC.
|
|
175
175
|
|
|
176
176
|
The updater accepts only matching state-schema and protocol contracts and an
|
|
177
|
-
unchanged deployment layout.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
unchanged deployment layout. Plugins may add command routes to existing services;
|
|
178
|
+
existing routes and all services, privileges, volumes and other deployment
|
|
179
|
+
configuration must remain unchanged. Incompatible changes fail before replacement,
|
|
180
|
+
even for an explicit candidate, and need a separately reviewed migration rather
|
|
181
|
+
than an override flag. Release authors must truthfully declare schema compatibility.
|
|
182
|
+
Code rollback **does not rewind private
|
|
181
183
|
state**, provider cursors or operation receipts; uncertain actions are never
|
|
182
184
|
replayed. Docker health is not proof of live provider identity or delivery.
|
|
183
185
|
|
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.33",
|
|
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/updates/control.mjs
CHANGED
|
@@ -44,6 +44,14 @@ export async function check(home) {
|
|
|
44
44
|
}
|
|
45
45
|
return results;
|
|
46
46
|
}
|
|
47
|
+
const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])])):value;
|
|
48
|
+
const same=(left,right)=>JSON.stringify(canonical(left))===JSON.stringify(canonical(right));
|
|
49
|
+
function additiveCommands(previous,next) {
|
|
50
|
+
const before={...previous,commands:{}},after={...next,commands:{}};
|
|
51
|
+
if(!same(before,after))return false;
|
|
52
|
+
for(const [name,command] of Object.entries(previous.commands||{}))if(!same(next.commands?.[name],command))return false;
|
|
53
|
+
return Object.values(next.commands||{}).every(command=>next.services?.[command.service]);
|
|
54
|
+
}
|
|
47
55
|
export async function eligibility(home,target,root,automatic) {
|
|
48
56
|
const old=await installed(home,target),pkg=await read(path.join(root,'package.json')),kind=target==='main'?'main':'plugin';
|
|
49
57
|
if(pkg.name!==old.pkg.name)throw Error('Package identity mismatch');
|
|
@@ -54,7 +62,9 @@ export async function eligibility(home,target,root,automatic) {
|
|
|
54
62
|
if(kind==='plugin') {
|
|
55
63
|
const s=await snapshot(root);revision=s.revision;
|
|
56
64
|
if(s.manifest.id!==target||s.manifest.version!==pkg.version)throw Error('Plugin identity/version mismatch');
|
|
57
|
-
if(
|
|
65
|
+
if(!additiveCommands(old.record.deployment,s.deployment))throw Error('Deployment permissions/layout changed; a separately reviewed migration is required');
|
|
66
|
+
const registry=await read(path.join(home,'registry.json'));
|
|
67
|
+
for(const alias of Object.keys(s.manifest.commands))if(registry.commands[alias]&®istry.commands[alias]!==target)throw Error('CLI alias collision');
|
|
58
68
|
}else {
|
|
59
69
|
for(const file of ['compose.yaml','compose.whatsapp.yaml'])if(!Buffer.from(await fs.readFile(path.join(root,file))).equals(await fs.readFile(path.join(old.root,file))))throw Error('Runtime deployment changed; a separately reviewed migration is required');
|
|
60
70
|
}
|
package/src/updates/runtime.mjs
CHANGED
|
@@ -122,7 +122,9 @@ export async function perform(home,job,hooks) {
|
|
|
122
122
|
for(const name of volumes)await backupVolume(run,old,name,backup);
|
|
123
123
|
await atomic(old.compose,await compose(config,candidate,secrets,home));
|
|
124
124
|
if(running)await run('docker',[...pluginArgs(candidate),'up','-d','--wait','--wait-timeout','90','--no-build']);
|
|
125
|
-
r.plugins[job.target]=candidate;
|
|
125
|
+
r.plugins[job.target]=candidate;
|
|
126
|
+
for(const alias of Object.keys(candidate.manifest.commands))r.commands[alias]=job.target;
|
|
127
|
+
await atomic(path.join(home,'registry.json'),r);
|
|
126
128
|
job.runtimeVerified=running;
|
|
127
129
|
}
|
|
128
130
|
job.status='completed';job.endedAt=new Date().toISOString();await save();return job;
|
package/test/updates.test.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { promisify } from 'node:util';
|
|
|
9
9
|
import { extract, digest, version, newer, compatible } from '../src/updates/artifact.mjs';
|
|
10
10
|
import { prepare, submit, command, read, jobPath, eligibility, jobs } from '../src/updates/control.mjs';
|
|
11
11
|
import { perform, environment, packageManager } from '../src/updates/runtime.mjs';
|
|
12
|
-
import { atomic, snapshot, compose } from '../src/plugins/manager.mjs';
|
|
12
|
+
import { atomic, snapshot, compose, prepareCommand } from '../src/plugins/manager.mjs';
|
|
13
13
|
import { bindUpdates } from '../src/updates/binding.mjs';
|
|
14
14
|
import { status as runtimeStatus } from '../src/updates/status.mjs';
|
|
15
15
|
const exec=promisify(execFile);
|
|
@@ -191,6 +191,29 @@ test('stopped plugins remain stopped; removed plugins and expanded mounts reject
|
|
|
191
191
|
const reg=await read(path.join(f.home,'registry.json'));delete reg.plugins.sample;await atomic(path.join(f.home,'registry.json'),reg);
|
|
192
192
|
await assert.rejects(prepare(f.home,'sample',{file:await f.pack()}),/not installed/);
|
|
193
193
|
});
|
|
194
|
+
test('plugin updates register additive command routes on an unchanged deployment',async t=>{
|
|
195
|
+
const f=await fixture(t,'plugin'),manifest=await read(path.join(f.source,'ez-plugin.json')),deployment=await read(path.join(f.source,'ez-deployment.json'));
|
|
196
|
+
manifest.commands.query={executable:'bin/example.mjs',args:[],channelQuery:true,exposure:{receivesExternalContent:true,sendsExternally:false,changesRecords:false,requiresReview:false}};
|
|
197
|
+
deployment.commands.query={service:'sample',argv:['node','/app/bin/example.mjs','query']};
|
|
198
|
+
await atomic(path.join(f.source,'ez-plugin.json'),manifest);await atomic(path.join(f.source,'ez-deployment.json'),deployment);
|
|
199
|
+
assert.equal((await eligibility(f.home,'sample',f.source,false)).pkg.version,'0.1.1');
|
|
200
|
+
const job=await queued(f),r=runtime(f);assert.equal((await perform(f.home,job,r)).status,'completed');
|
|
201
|
+
const registry=await read(path.join(f.home,'registry.json'));assert.equal(registry.commands.query,'sample');
|
|
202
|
+
const dispatch=await prepareCommand(f.home,'query',[]);assert.deepEqual(dispatch.argv.slice(-4),['node','sample','/app/bin/example.mjs','query']);
|
|
203
|
+
assert(r.calls.some(call=>call.includes('up')));
|
|
204
|
+
});
|
|
205
|
+
test('plugin command additions ignore object key order but reject collisions and changed routes',async t=>{
|
|
206
|
+
const f=await fixture(t,'plugin'),deployment=await read(path.join(f.source,'ez-deployment.json'));
|
|
207
|
+
deployment.services.sample={healthcheck:deployment.services.sample.healthcheck,volumes:deployment.services.sample.volumes,buildTarget:deployment.services.sample.buildTarget};
|
|
208
|
+
await atomic(path.join(f.source,'ez-deployment.json'),deployment);assert.equal((await eligibility(f.home,'sample',f.source,false)).pkg.version,'0.1.1');
|
|
209
|
+
const manifest=await read(path.join(f.source,'ez-plugin.json'));manifest.commands.other={executable:'bin/example.mjs',args:[]};deployment.commands.other={service:'sample',argv:['node','/app/bin/example.mjs','other']};
|
|
210
|
+
await atomic(path.join(f.source,'ez-plugin.json'),manifest);await atomic(path.join(f.source,'ez-deployment.json'),deployment);
|
|
211
|
+
const registry=await read(path.join(f.home,'registry.json'));registry.commands.other='different';await atomic(path.join(f.home,'registry.json'),registry);
|
|
212
|
+
await assert.rejects(eligibility(f.home,'sample',f.source,false),/alias collision/);
|
|
213
|
+
delete registry.commands.other;await atomic(path.join(f.home,'registry.json'),registry);
|
|
214
|
+
deployment.commands.sample.argv.push('--changed');await atomic(path.join(f.source,'ez-deployment.json'),deployment);
|
|
215
|
+
await assert.rejects(eligibility(f.home,'sample',f.source,false),/deployment/i);
|
|
216
|
+
});
|
|
194
217
|
test('interrupted activation recovers previous code; rollback failure is explicit and blocks further jobs',async t=>{
|
|
195
218
|
const f=await fixture(t),job=await queued(f),r=runtime(f);await perform(f.home,job,r);
|
|
196
219
|
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|