@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 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. Mandatory Adversarial & Negative Tests
101
- Every pull request modifying authority, execution, or routing must include negative tests:
102
- 1. Unapproved Telegram ID $\rightarrow$ no process spawned.
103
- 2. Non-owner group message $\rightarrow$ silent ignore; owner group discovery $\rightarrow$ private destination only.
104
- 3. Executor environment check $\rightarrow$ asserts `TELEGRAM_BOT_TOKEN` is `undefined`.
105
- 4. Corrupt JSON in store $\rightarrow$ handled gracefully without process crash.
106
- 5. Injected path traversal in IDs $\rightarrow$ rejected.
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
- Maintainers and external agents use the same PR, tests and documentation standard.
112
- Keep internal plans and private evidence outside this repository.
113
-
114
- Before edits, follow CONTRIBUTING.md's isolated-work rules: one task per dedicated
115
- worktree/branch/PR, starting from fetched origin/main. Do not switch or mix work in
116
- another task's checkout. Stage only this task's changes. Keep its worktree through
117
- review and QA; independent review and green CI precede an authorized merge.
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, then complete the
20
- maintainer-authorized merge. Maintainers use PRs too. No CLA, ticket
21
- requirement, custom commit format or additional approval committee.
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 draft PR as soon as the first coherent change
83
- is reviewable, before reporting implementation complete. Do not leave completed
84
- work only in a local branch. Continue through the release handoff below;
85
- implementation completion alone does not authorize publication.
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, applicable QA and maintainer authorization.
102
- Publish only under the separate release process; commits and merges are not
103
- releases. Record which source commit and artifact hash were tested.
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's request is authorization for the requested work and its normal
128
- implementation steps. Do not ask them to approve the same work again. Once the
129
- applicable gates pass, execute the requested merge/release and report the packages,
130
- versions, channel, verification and material limits. Batch related packages in
131
- dependency order. Prepare the reviewed commits, artifacts and checksums before
132
- shipping. A request to merge does not silently expand to publication or changing
133
- a private package's visibility; a request to release already authorizes release.
134
- If shipping was not requested, report readiness and the next step without treating
135
- every completed feature as permission to publish.
136
-
137
- Complete the authorized merge, publication and rollout using
138
- docs/releasing.md, then verify registry metadata, downloaded artifact and the
139
- installed runtime. Report the outcome. Escalate only a product decision, missing
140
- credential/2FA, failed gate that cannot be repaired in scope, or material scope
141
- change. Human attention belongs on product intent;
142
- the agent operates the technical workflow.
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
- Do not copy private QA, company policy, proposed features or unsupported claims.
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. Changes to privileges, services, volumes or Compose
178
- configuration fail before replacement, even for an explicit candidate. These
179
- need a separately reviewed migration, not an override flag. Release authors must
180
- truthfully declare schema compatibility. Code rollback **does not rewind private
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.32",
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.",
@@ -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(JSON.stringify(s.deployment)!==JSON.stringify(old.record.deployment))throw Error('Deployment permissions/layout changed; a separately reviewed migration is required');
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]&&registry.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
  }
@@ -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;await atomic(path.join(home,'registry.json'),r);
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;
@@ -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';