@kybernesis/create 0.12.3 → 0.13.1
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/dist/doctor.js +52 -1
- package/dist/templates.js +23 -2
- package/dist/upgrade.js +65 -1
- package/package.json +1 -1
- package/skills/fde-engagement/references/playbook.md +17 -3
package/dist/doctor.js
CHANGED
|
@@ -275,6 +275,53 @@ export async function doctor() {
|
|
|
275
275
|
else {
|
|
276
276
|
add("fail", `self-hosted: ${shortQueueTimeouts.join(" and ")} left at the 30s default`, "one queue delivery holds a connection open for the entire turn, so any turn slower than the timeout is redelivered and its steps re-run — the agent answers the same question twice, with two different answers, and nothing reports an error. Set both to 900000 in .env.local and restart the server");
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Disk, on a host whose runtime does not clean up after itself.
|
|
280
|
+
*
|
|
281
|
+
* eve builds a sandbox template per session configuration and keeps every
|
|
282
|
+
* one, and leaves session containers running long after their turn ended.
|
|
283
|
+
* The result is gigabytes a day on a working agent, and the failure it
|
|
284
|
+
* eventually produces looks like anything except a full disk.
|
|
285
|
+
*/
|
|
286
|
+
if (capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes") {
|
|
287
|
+
const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
|
|
288
|
+
const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
|
|
289
|
+
if (job !== "yes") {
|
|
290
|
+
add("warn", "no daily docker reclaim on this host", "sandbox images and abandoned session containers accumulate by the gigabyte; `kyb upgrade` installs /etc/cron.daily/kyb-docker-prune");
|
|
291
|
+
}
|
|
292
|
+
else if (percent >= 80) {
|
|
293
|
+
add("fail", `disk ${percent}% full despite the reclaim job`, "run it now: sudo /etc/cron.daily/kyb-docker-prune, and check what else is on this host");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
add("pass", `daily docker reclaim installed (disk ${percent}% used)`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Instructions that name a tool the agent does not have.
|
|
301
|
+
*
|
|
302
|
+
* `deliver` mounts with the engineer layer, which is mounted on the builder
|
|
303
|
+
* subagent — so an agent whose ROOT is told to hand over files (by its own
|
|
304
|
+
* instructions, or by our playbook, which says to) promises a file it
|
|
305
|
+
* cannot send. It cannot see the mismatch either: it writes the contents to
|
|
306
|
+
* a memory note, or apologises without a cause, and the delivery
|
|
307
|
+
* infrastructure passes every check while nobody can receive anything.
|
|
308
|
+
*/
|
|
309
|
+
const rootDeliver = existsSync(join(cwd, "agent/tools/deliver.ts"));
|
|
310
|
+
const instructionsMentionDeliver = (() => {
|
|
311
|
+
try {
|
|
312
|
+
const dir = join(cwd, "agent/instructions");
|
|
313
|
+
return readdirSync(dir).some((file) => readFileSync(join(dir, file), "utf8").includes("deliver"));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
if (instructionsMentionDeliver && !rootDeliver) {
|
|
320
|
+
add("fail", "instructions tell this agent to deliver files, but the root has no deliver tool", 'add agent/tools/deliver.ts: export { deliver as default } from "@kybernesis/engineer/tools" — the engineer layer mounts it on the builder subagent only');
|
|
321
|
+
}
|
|
322
|
+
else if (rootDeliver) {
|
|
323
|
+
add("pass", "the agent that talks to people can also hand them a file");
|
|
324
|
+
}
|
|
278
325
|
// The exe VM sandbox backend needs a credential that cannot be scoped.
|
|
279
326
|
// Surface the blast radius here, where it is still cheap to change course.
|
|
280
327
|
const sandboxFile = join(cwd, "agent/sandbox/sandbox.ts");
|
|
@@ -350,7 +397,11 @@ export async function doctor() {
|
|
|
350
397
|
add("fail", "engineer subagent has NO sandbox of its own", "subagents do not inherit the root sandbox — add agent/subagents/builder/sandbox/sandbox.ts or the vision loop cannot run");
|
|
351
398
|
}
|
|
352
399
|
if (existsSync(join(builderDir, "extensions/engineer.ts"))) {
|
|
353
|
-
|
|
400
|
+
// Not "the root keeps no shell": every eve agent has a sandbox with bash,
|
|
401
|
+
// read_file, write_file, glob and grep. What the local mount withholds is
|
|
402
|
+
// the build loop and any shell ON THE HOST. This sentence is one people
|
|
403
|
+
// reason about security boundaries from, so it says the true thing.
|
|
404
|
+
add("pass", "engineer mounted locally on the subagent (root gets no host shell, no build loop)");
|
|
354
405
|
}
|
|
355
406
|
else {
|
|
356
407
|
add("warn", "engineer extension not mounted on the subagent", "agent/subagents/builder/extensions/engineer.ts");
|
package/dist/templates.js
CHANGED
|
@@ -513,18 +513,39 @@ export default defineAgent({
|
|
|
513
513
|
"Builds and runs software: scaffolds projects, writes code, installs dependencies, runs builds and dev servers, and visually verifies rendered pages. Use when the user asks for something to be BUILT, prototyped, deployed, or fixed in code — not for questions, planning, or scheduling.",
|
|
514
514
|
model: ${JSON.stringify(model)},
|
|
515
515
|
});
|
|
516
|
+
`,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
path: "agent/tools/deliver.ts",
|
|
520
|
+
content: `// Handing a file to the person you are talking to.
|
|
521
|
+
//
|
|
522
|
+
// The engineer layer mounts on \`builder\`, so building stays there — but
|
|
523
|
+
// DELIVERING is not building. It reads one file from this agent's own sandbox
|
|
524
|
+
// and copies it to storage, runs no commands and writes nothing back, and the
|
|
525
|
+
// agent that needs it is the one talking to the user.
|
|
526
|
+
//
|
|
527
|
+
// Without this the root is told (by its instructions, and by our own playbook)
|
|
528
|
+
// to deliver files and has no such tool. It cannot detect the mismatch, so it
|
|
529
|
+
// promises a file and then quietly puts the contents somewhere nobody can
|
|
530
|
+
// reach — a memory note, or an apology with no cause given.
|
|
531
|
+
export { deliver as default } from "@kybernesis/engineer/tools";
|
|
516
532
|
`,
|
|
517
533
|
},
|
|
518
534
|
{
|
|
519
535
|
path: "agent/subagents/builder/extensions/engineer.ts",
|
|
520
536
|
content: `// Engineer layer mounted LOCALLY on this subagent (eve >=0.30): screenshot,
|
|
521
537
|
// deliver, and the trade-school skills belong to \`builder\` alone, so the root
|
|
522
|
-
//
|
|
538
|
+
// gets no shell ON THE HOST and none of the build loop.
|
|
539
|
+
//
|
|
540
|
+
// Not "the root has no shell" — every eve agent has a sandbox with bash,
|
|
541
|
+
// read_file, write_file, glob and grep, and this app builds two templates
|
|
542
|
+
// (root and builder). The root's shell is its own container. The distinction
|
|
543
|
+
// that matters is host access, and the old wording blurred exactly the line
|
|
544
|
+
// people read this comment to understand.
|
|
523
545
|
//
|
|
524
546
|
// The root DOES get a browser and GitHub tools — the engineer layer installs
|
|
525
547
|
// extension/agent-browser and extension/github-tools at the root, deliberately,
|
|
526
548
|
// because reading a page is not the same blast radius as running a command.
|
|
527
|
-
// This comment used to claim otherwise, which is worse than saying nothing.
|
|
528
549
|
export { default } from "@kybernesis/engineer";
|
|
529
550
|
`,
|
|
530
551
|
},
|
package/dist/upgrade.js
CHANGED
|
@@ -179,14 +179,63 @@ function repairBuzzSetup(cwd, deps) {
|
|
|
179
179
|
console.log(` ${dim("Takes effect after the next build and restart.")}\n`);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Keep the agent host from filling up with what the runtime leaves behind.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* eve builds a sandbox template image per session configuration and never
|
|
187
|
+
* collects the old ones, and it leaves session CONTAINERS running — a turn that
|
|
188
|
+
* took four minutes can still own a container, and its writable layer, eight
|
|
189
|
+
* days later. Forty stale images accumulated on one agent in a week; another
|
|
190
|
+
* reached 94% full and began failing in ways that looked like anything but a
|
|
191
|
+
* disk problem. Every self-hosted agent hits this; it is a property of the
|
|
192
|
+
* runtime, not of any one deployment.
|
|
193
|
+
*
|
|
194
|
+
* Installed rather than documented, and DAILY rather than weekly, because the
|
|
195
|
+
* accumulation is measured in gigabytes per day on an agent doing real work.
|
|
196
|
+
* The first version of this ran weekly and was already too slow.
|
|
197
|
+
*/
|
|
198
|
+
function repairDockerPrune(cwd, deps) {
|
|
199
|
+
if (!deps["@kybernesis/exe"])
|
|
200
|
+
return;
|
|
201
|
+
if (capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() !== "yes")
|
|
202
|
+
return;
|
|
203
|
+
const installed = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"]);
|
|
204
|
+
if (installed?.trim() === "yes")
|
|
205
|
+
return;
|
|
206
|
+
const source = join(cwd, "node_modules/@kybernesis/exe/scripts/docker-prune.sh");
|
|
207
|
+
if (!existsSync(source))
|
|
208
|
+
return;
|
|
209
|
+
// `sudo -n`: this runs inside an upgrade, and an upgrade that stops to ask
|
|
210
|
+
// for a password in the middle of an unattended run is worse than one that
|
|
211
|
+
// says what it could not do.
|
|
212
|
+
const ok = run("sh", [
|
|
213
|
+
"-c",
|
|
214
|
+
// The weekly predecessor is removed in the same breath: two jobs pruning
|
|
215
|
+
// the same host is not twice as safe, it is one more thing to reason
|
|
216
|
+
// about when something unexpected disappears.
|
|
217
|
+
`sudo -n cp ${JSON.stringify(source)} /etc/cron.daily/kyb-docker-prune && ` +
|
|
218
|
+
`sudo -n chmod 755 /etc/cron.daily/kyb-docker-prune && ` +
|
|
219
|
+
`sudo -n rm -f /etc/cron.weekly/docker-prune`,
|
|
220
|
+
], { cwd, allowFail: true, quiet: true });
|
|
221
|
+
if (ok) {
|
|
222
|
+
console.log(` ${green("+")} installed the daily docker reclaim (/etc/cron.daily/kyb-docker-prune)`);
|
|
223
|
+
console.log(` ${dim("eve leaves sandbox images and running session containers behind; this collects them.")}\n`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(` ${yellow("!")} could not install the docker reclaim job (needs sudo). Without it this host ` +
|
|
227
|
+
`fills with stale sandbox images. Run:\n` +
|
|
228
|
+
` ${dim("sudo cp node_modules/@kybernesis/exe/scripts/docker-prune.sh /etc/cron.daily/kyb-docker-prune && sudo chmod 755 /etc/cron.daily/kyb-docker-prune")}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
182
231
|
export async function upgrade(skipEval) {
|
|
183
232
|
const cwd = process.cwd();
|
|
184
233
|
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
185
234
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
186
235
|
console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
|
|
187
236
|
warnIfStale();
|
|
237
|
+
// Env-only, so it is safe before anything is installed.
|
|
188
238
|
repairLocalQueueTimeouts(cwd, deps);
|
|
189
|
-
repairBuzzSetup(cwd, deps);
|
|
190
239
|
const toUpgrade = [];
|
|
191
240
|
const unresolved = [];
|
|
192
241
|
for (const name of kybernesisPackages(deps)) {
|
|
@@ -241,10 +290,25 @@ export async function upgrade(skipEval) {
|
|
|
241
290
|
}
|
|
242
291
|
if (toUpgrade.length === 0) {
|
|
243
292
|
console.log(`\n${green("Everything is at latest certified versions.")}\n`);
|
|
293
|
+
// Still repair: being on the right versions is not the same as being set
|
|
294
|
+
// up. An agent can sit at latest for weeks with a capability switched off.
|
|
295
|
+
repairBuzzSetup(cwd, deps);
|
|
296
|
+
repairDockerPrune(cwd, deps);
|
|
244
297
|
return;
|
|
245
298
|
}
|
|
246
299
|
console.log(bold(`\nInstalling: ${toUpgrade.join(", ")}\n`));
|
|
247
300
|
run("npm", ["install", ...toUpgrade], { cwd });
|
|
301
|
+
/**
|
|
302
|
+
* Repairs run AFTER the install, not before.
|
|
303
|
+
*
|
|
304
|
+
* Both of these copy files out of packages that the install has just put
|
|
305
|
+
* there — a proxy script, a cron job, a CLI. Running them first meant looking
|
|
306
|
+
* for a file the older installed version did not ship: the repair found
|
|
307
|
+
* nothing, said nothing, and only worked on the NEXT upgrade. Which is a
|
|
308
|
+
* bug that hides itself, because by then it looks like it always worked.
|
|
309
|
+
*/
|
|
310
|
+
repairBuzzSetup(cwd, deps);
|
|
311
|
+
repairDockerPrune(cwd, deps);
|
|
248
312
|
run("npm", ["run", "typecheck"], { cwd });
|
|
249
313
|
if (eveChanged) {
|
|
250
314
|
// A framework bump must also pass discovery/compile, not just types.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -2052,9 +2052,23 @@ tool. Reads went from unreliable to deterministic with about sixty lines.
|
|
|
2052
2052
|
### 11.6 The engineer layer, self-hosted
|
|
2053
2053
|
|
|
2054
2054
|
`--engineer` scaffolds a **builder subagent** that owns the build capability, so
|
|
2055
|
-
the root agent
|
|
2056
|
-
workshop sandbox, Playwright, screenshots, visual
|
|
2057
|
-
reduced version of the Vercel one.
|
|
2055
|
+
the root agent gets **no shell on the host and no build loop**. It comes with the
|
|
2056
|
+
full production loop — workshop sandbox, Playwright, screenshots, visual
|
|
2057
|
+
verification, delivery — not a reduced version of the Vercel one.
|
|
2058
|
+
|
|
2059
|
+
> Say it that way rather than "the root never gets a shell". Every eve agent has
|
|
2060
|
+
> a sandbox with `bash`, `read_file`, `write_file`, `glob` and `grep`, and an
|
|
2061
|
+
> `--engineer` app builds two templates. The root's shell is its own container.
|
|
2062
|
+
> The line that matters is host access, and the shorter phrasing blurs exactly
|
|
2063
|
+
> the boundary people quote this passage to explain.
|
|
2064
|
+
|
|
2065
|
+
**Delivery belongs to the root as well.** The engineer layer mounts on `builder`,
|
|
2066
|
+
but the agent that talks to people is the one asked for files — and delivering
|
|
2067
|
+
reads one file and copies it, which is not the blast radius that justified
|
|
2068
|
+
keeping the build loop away from the root. `kyb init --engineer` now scaffolds
|
|
2069
|
+
`agent/tools/deliver.ts` for exactly this. An agent instructed to hand over files
|
|
2070
|
+
without that mount cannot tell it is missing: it writes the contents to a memory
|
|
2071
|
+
note and reports success.
|
|
2058
2072
|
|
|
2059
2073
|
- **Subagents own their sandbox; they do NOT inherit the root's.** A builder
|
|
2060
2074
|
without its own `sandbox/sandbox.ts` gets a bare template and every screenshot
|