@kybernesis/create 0.16.0 → 0.17.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/README.md CHANGED
@@ -97,7 +97,7 @@ Before an eve version change, upgrade reads `.eve/.workflow-data/runs`, prints
97
97
  sessions when available, and requires explicit confirmation. Noninteractive
98
98
  runs fail closed unless `--yes` (or `-y`) is supplied. The warning is still printed with
99
99
  `--yes` and is not bypassed by `--skip-eval`. Existing direct `eve eval` scripts
100
- are migrated to `kyb-eval`; custom scripts are left untouched with a manual fix.
100
+ are migrated to `kyb-eval` (shipped as a binary of this package from 0.17); custom scripts are left untouched with a manual fix.
101
101
  The wrapper counts corruption diagnostics beside the eval result and turns a
102
102
  nominally green eve exit into a failure when condemned durable state was seen.
103
103
  It also identifies failures from `judge.autoevals.*` assertions as judge
@@ -10,7 +10,10 @@ export const CERTIFIED_INHERITED_MAX_INPUT_TOKENS_PER_SESSION = 40_000_000;
10
10
  export function parseEveInfoInspection(output, readText = (path) => readFileSync(path, "utf8")) {
11
11
  let info;
12
12
  try {
13
- info = JSON.parse(output);
13
+ // eve ≥0.62 prints its terminal header ("☰eve v0.68.0 …") ahead of the
14
+ // JSON even with --json, so parse from the first object literal.
15
+ const start = output.indexOf("{");
16
+ info = JSON.parse(start > 0 ? output.slice(start) : output);
14
17
  }
15
18
  catch (error) {
16
19
  return unresolved(`eve info --json was not valid JSON: ${error.message}`);
package/dist/cli.js CHANGED
@@ -64,6 +64,7 @@ function initOptions(rest) {
64
64
  const subs = flag(rest, 'subagents');
65
65
  return {
66
66
  engineer: rest.includes('--engineer'),
67
+ computer: rest.includes('--computer'),
67
68
  studio: rest.includes('--studio'),
68
69
  channel: flag(rest, "channel"),
69
70
  host: flag(rest, "host"),
@@ -104,6 +105,7 @@ function usage(command) {
104
105
  --model-reach=<reach> ${dim("claude-sub (exe only; default: host route)")}
105
106
  --subagents=a,b ${dim("department subagents")}
106
107
  --engineer ${dim("add the engineer layer")}
108
+ --computer ${dim("the agent's own desktop + Chrome (exe only)")}
107
109
  --studio ${dim("wire KYBER Studio local/manage routes")}
108
110
  --yes, -y ${dim("no prompts")}
109
111
 
@@ -228,6 +230,7 @@ ${dim(" npm i -g @kybernesis/create@latest")}
228
230
  --model=<id> ${dim("provider/model-id; bare claude-opus-5 for claude-sub")}
229
231
  --model-reach=<r> ${dim("claude-sub (exe only); env: KYB_MODEL_REACH")}
230
232
  --engineer ${dim("add the engineer layer: workshop sandbox + vision dev loop")}
233
+ --computer ${dim("the agent's own computer: persistent desktop + Chrome you can watch (exe only)")}
231
234
  --studio ${dim("wire for KYBER Studio: local execution + management routes")}
232
235
  --yes ${dim("no prompts; take flags and defaults")}
233
236
  ${bold("kyb add channel <kind>")}
package/dist/doctor.d.ts CHANGED
@@ -32,4 +32,21 @@ export declare function diskUsageDoctorCheck(percent: number, dockerSummary: str
32
32
  */
33
33
  export declare function inspectDockerTemplatesViaExe(cwd: string): Promise<DockerTemplateInspection | null>;
34
34
  export declare function doctor(): Promise<void>;
35
+ export interface ComputerInspection {
36
+ /** agent/sandbox.ts imports @kybernesis/computer. */
37
+ configured: boolean;
38
+ /** COMPUTER_NAME from .env.local, or the default the sandbox file falls back to. */
39
+ name: string;
40
+ containerState: "running" | "stopped" | "missing" | "unknown";
41
+ /** The noVNC port is bound on loopback. */
42
+ screenBound: boolean | null;
43
+ vncPasswordSet: boolean;
44
+ }
45
+ /**
46
+ * A computer that is configured but not running is the quiet failure here:
47
+ * every session would try to start it and the first turn pays the whole boot,
48
+ * or fails if Docker is off. Missing before the first build is normal.
49
+ */
50
+ export declare function computerDoctorChecks(c: ComputerInspection): Check[];
51
+ export declare function inspectComputer(cwd: string): ComputerInspection | null;
35
52
  export {};
package/dist/doctor.js CHANGED
@@ -313,7 +313,7 @@ export async function doctor() {
313
313
  const vercelConn = join(cwd, "agent/connections/vercel.ts");
314
314
  const selfHostedAgent = Boolean(deps["@kybernesis/exe"]) ||
315
315
  (existsSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts")) &&
316
- readFileSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts"), "utf8").includes("docker("));
316
+ /docker\(|DockerSandbox/.test(readFileSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts"), "utf8")));
317
317
  if (selfHostedAgent && !existsSync(vercelConn)) {
318
318
  add("pass", "no Vercel MCP connection (self-hosted)", "public deploys need the CLIENT's own Vercel token — Vercel Connect does not work off-Vercel");
319
319
  }
@@ -399,7 +399,7 @@ export async function doctor() {
399
399
  // deployment. None of them are theoretical.
400
400
  const selfHosted = Boolean(deps["@kybernesis/exe"]) ||
401
401
  existsSync(join(cwd, "agent/sandbox/sandbox.ts")) &&
402
- readFileSync(join(cwd, "agent/sandbox/sandbox.ts"), "utf8").includes("docker(");
402
+ /docker\(|DockerSandbox/.test(readFileSync(join(cwd, "agent/sandbox/sandbox.ts"), "utf8"));
403
403
  const templateInspection = await inspectDockerTemplatesViaExe(cwd);
404
404
  for (const check of templateInspection ? dockerTemplateDoctorChecks(templateInspection) : []) {
405
405
  add(check.verdict, check.label, check.detail);
@@ -508,6 +508,10 @@ export async function doctor() {
508
508
  ]);
509
509
  const diskCheck = diskUsageDoctorCheck(percent, dockerSummary);
510
510
  add(diskCheck.verdict, diskCheck.label, diskCheck.detail);
511
+ const computer = inspectComputer(cwd);
512
+ if (computer)
513
+ for (const check of computerDoctorChecks(computer))
514
+ checks.push(check);
511
515
  }
512
516
  /**
513
517
  * Instructions that name a tool the agent does not have.
@@ -679,3 +683,69 @@ export async function doctor() {
679
683
  console.log(`\n ${fails ? red(`${fails} failing`) : green("0 failing")}, ${warns ? yellow(`${warns} warnings`) : "0 warnings"}\n`);
680
684
  process.exit(fails ? 1 : 0);
681
685
  }
686
+ /**
687
+ * A computer that is configured but not running is the quiet failure here:
688
+ * every session would try to start it and the first turn pays the whole boot,
689
+ * or fails if Docker is off. Missing before the first build is normal.
690
+ */
691
+ export function computerDoctorChecks(c) {
692
+ if (!c.configured)
693
+ return [];
694
+ const checks = [];
695
+ switch (c.containerState) {
696
+ case "running":
697
+ checks.push({ verdict: "pass", label: `the agent's computer "${c.name}" is running` });
698
+ break;
699
+ case "stopped":
700
+ checks.push({
701
+ verdict: "warn",
702
+ label: `the agent's computer "${c.name}" exists but is stopped`,
703
+ detail: "the next session restarts it and pays the boot; `docker start " + c.name + "` brings it back now",
704
+ });
705
+ break;
706
+ case "missing":
707
+ checks.push({
708
+ verdict: "warn",
709
+ label: `the agent's computer "${c.name}" has not been built yet`,
710
+ detail: "`eve build` creates and prepares it (eve's desktop stack + Chrome, ~5 minutes)",
711
+ });
712
+ break;
713
+ default:
714
+ checks.push({ verdict: "warn", label: "could not inspect the agent's computer", detail: "is Docker running on this host?" });
715
+ }
716
+ if (c.containerState === "running" && c.screenBound === false) {
717
+ checks.push({
718
+ verdict: "fail",
719
+ label: "the computer is running but its screen is not published on :6080",
720
+ detail: "the container was created without the noVNC port mapping; `docker rm -f " + c.name + "` and `eve build` to recreate it (volumes keep the home and /workspace)",
721
+ });
722
+ }
723
+ if (!c.vncPasswordSet) {
724
+ checks.push({
725
+ verdict: "warn",
726
+ label: "the computer's screen has no VNC password",
727
+ detail: "exe's port-proxy sign-in is the only gate; set COMPUTER_VNC_PASSWORD in .env.local and `docker rm -f " + c.name + "` + `eve build` to apply",
728
+ });
729
+ }
730
+ return checks;
731
+ }
732
+ export function inspectComputer(cwd) {
733
+ const sandboxPath = join(cwd, "agent/sandbox.ts");
734
+ if (!existsSync(sandboxPath) || !readFileSync(sandboxPath, "utf8").includes("@kybernesis/computer"))
735
+ return null;
736
+ const envPath = join(cwd, ".env.local");
737
+ const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
738
+ const sandboxSource = readFileSync(sandboxPath, "utf8");
739
+ const fallback = /COMPUTER_NAME \?\? "([^"]+)"/.exec(sandboxSource)?.[1] ?? "agent-computer";
740
+ const name = env.COMPUTER_NAME?.trim() || fallback;
741
+ const state = capture("docker", ["inspect", "-f", "{{.State.Running}}", name]);
742
+ const containerState = state === null ? "unknown" : state.trim() === "true" ? "running" : state.trim() === "false" ? "stopped" : "missing";
743
+ const ports = containerState === "running" ? capture("docker", ["port", name, "6080"]) : null;
744
+ return {
745
+ configured: true,
746
+ name,
747
+ containerState,
748
+ screenBound: containerState === "running" ? Boolean(ports && ports.trim().length > 0) : null,
749
+ vncPasswordSet: Boolean(env.COMPUTER_VNC_PASSWORD?.trim()),
750
+ };
751
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `kyb-eval`: `eve eval` with the two checks eve's own exit code does not make.
4
+ *
5
+ * `kyb upgrade` migrates every agent's `scripts.eval` to this command and runs
6
+ * it as the deploy gate, and the README has described it since 0.14 — but no
7
+ * binary ever shipped, so a migrated agent's `npm run eval` failed with
8
+ * "command not found" and the gate reported red for the wrong reason.
9
+ *
10
+ * What it adds on top of `eve eval <args>`:
11
+ * - a nominally green run that logged condemned durable state
12
+ * (`CORRUPTED_EVENT_LOG`, `REPLAY_DIVERGENCE`) exits 1 — those runs are
13
+ * terminal and a suite that passed beside them proves less than it says;
14
+ * - a run whose failures are all judge errors (evaluation model unreachable,
15
+ * HTTP failures from the judge provider) says so, rather than reading as the
16
+ * agent under test failing.
17
+ */
18
+ import { spawn } from "node:child_process";
19
+ const CONDEMNED = /CORRUPTED_EVENT_LOG|REPLAY_DIVERGENCE/;
20
+ const JUDGE_FAILURE = /judge[^\n]*(unreachable|ECONNREFUSED|fetch failed|HTTP \d{3}|Unauthenticated|no credentials|not valid)/i;
21
+ function main() {
22
+ const args = process.argv.slice(2);
23
+ const child = spawn("npx", ["eve", "eval", ...args], { stdio: ["inherit", "pipe", "pipe"], env: process.env });
24
+ let condemned = 0;
25
+ let judgeFailures = 0;
26
+ const scan = (chunk, out) => {
27
+ const text = chunk.toString();
28
+ out.write(text);
29
+ for (const line of text.split("\n")) {
30
+ if (CONDEMNED.test(line))
31
+ condemned += 1;
32
+ if (JUDGE_FAILURE.test(line))
33
+ judgeFailures += 1;
34
+ }
35
+ };
36
+ child.stdout.on("data", (chunk) => scan(chunk, process.stdout));
37
+ child.stderr.on("data", (chunk) => scan(chunk, process.stderr));
38
+ child.on("error", (error) => {
39
+ console.error(`kyb-eval: could not start eve eval: ${error.message}`);
40
+ process.exit(2);
41
+ });
42
+ child.on("close", (code) => {
43
+ if (condemned > 0) {
44
+ console.error(`\nkyb-eval: ${condemned} condemned durable-state diagnostic(s) seen (CORRUPTED_EVENT_LOG / REPLAY_DIVERGENCE). ` +
45
+ "Those conversations are terminal; treating this run as red.");
46
+ process.exit(1);
47
+ }
48
+ if (code !== 0 && judgeFailures > 0) {
49
+ console.error(`\nkyb-eval: ${judgeFailures} judge failure(s) seen — the evaluation model was unreachable or refused, ` +
50
+ "which is a judge-reachability problem, not evidence that the agent under test failed. Fix the judge, then rerun.");
51
+ }
52
+ process.exit(code ?? 1);
53
+ });
54
+ }
55
+ main();
package/dist/init.d.ts CHANGED
@@ -6,6 +6,8 @@ import { type ChannelKind, type HostKind } from "./templates.js";
6
6
  export declare function finalizeGithubToolsRegistryMount(cwd: string, registryAddSucceeded: boolean): boolean;
7
7
  export interface InitOptions {
8
8
  engineer?: boolean;
9
+ /** The agent's own computer: a persistent Docker desktop with Chrome (exe hosts only). */
10
+ computer?: boolean;
9
11
  /**
10
12
  * Wire this agent for KYBER Studio: local execution on the user's own machine,
11
13
  * and management routes so Studio can install capabilities and write routines.
package/dist/init.js CHANGED
@@ -1,7 +1,7 @@
1
- import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, red, yellow, } from "./util.js";
4
- import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, githubToolsMountTs, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
4
+ import { CHANNEL_KINDS, channelPlan, computerPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, githubToolsMountTs, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
5
5
  import { suiteDir } from "./skills.js";
6
6
  import { configureArcana } from "./arcana.js";
7
7
  import { upsertEnv } from "./envfile.js";
@@ -36,6 +36,7 @@ export function finalizeGithubToolsRegistryMount(cwd, registryAddSucceeded) {
36
36
  }
37
37
  export async function init(rawName, options = {}) {
38
38
  const engineer = options.engineer === true;
39
+ const computer = options.computer === true;
39
40
  const studio = options.studio === true;
40
41
  const nonInteractive = options.yes === true;
41
42
  const name = slug(rawName ?? (await ask("Agent name (kebab-case)?", "acme-agent")));
@@ -187,6 +188,14 @@ export async function init(rawName, options = {}) {
187
188
  console.log(yellow(` ! ${item} did not install cleanly — re-run: npx eve add ${item}`));
188
189
  }
189
190
  }
191
+ const compPlan = computer && host === "exe" ? computerPlan(name) : null;
192
+ if (computer && host !== "exe") {
193
+ console.log(yellow("\n ! --computer needs a Docker host (exe.dev); a Vercel function cannot keep a desktop alive. Skipped."));
194
+ }
195
+ if (compPlan) {
196
+ console.log(bold("\n2e The agent's computer: persistent desktop + Chrome, sighted computer tool, browser guard …"));
197
+ run("npm", ["install", ...compPlan.deps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
198
+ }
190
199
  console.log(bold("\n2d Seeding the FDE Claude Code skill suite (.claude/skills) …"));
191
200
  try {
192
201
  cpSync(suiteDir(), join(dir, ".claude/skills"), { recursive: true });
@@ -373,8 +382,17 @@ export async function init(rawName, options = {}) {
373
382
  writeFileSync(full, file.content);
374
383
  }
375
384
  }
385
+ if (compPlan) {
386
+ for (const file of compPlan.files) {
387
+ const full = join(dir, file.path);
388
+ mkdirSync(join(full, ".."), { recursive: true });
389
+ writeFileSync(full, file.content);
390
+ }
391
+ }
376
392
  console.log(bold("\n5/6 Env template + hermetic eval script …"));
377
393
  writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer, plan.env, host, model));
394
+ if (compPlan)
395
+ appendFileSync(join(dir, ".env.example"), compPlan.env);
378
396
  const pkgPath = join(dir, "package.json");
379
397
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
380
398
  pkg.scripts = { ...pkg.scripts, eval: evalScript(name, depts) };
@@ -394,6 +412,7 @@ export async function init(rawName, options = {}) {
394
412
  model.reach === "claude-sub" ? "Claude subscription reach" : null,
395
413
  studio ? "KYBER Studio (local execution + management routes)" : null,
396
414
  engineer ? "engineer subagent (workshop + vision loop)" : null,
415
+ compPlan ? "its own computer (desktop + Chrome, watch and take over)" : null,
397
416
  depts.length ? `${depts.length} dept subagent(s)` : null,
398
417
  ].filter(Boolean);
399
418
  const steps = [
@@ -401,6 +420,7 @@ export async function init(rawName, options = {}) {
401
420
  ...hostSteps(host, name, model),
402
421
  ...plan.steps,
403
422
  ...(engPlan?.steps ?? []),
423
+ ...(compPlan?.steps ?? []),
404
424
  `Control plane: register agent "${name}" at ${issuer}/agents + grant the pilot cohort`,
405
425
  `npm run eval → green → deploy → live smoke + the revoke demo`,
406
426
  ];
@@ -27,3 +27,15 @@ export interface RemovedDefaultToolsRepair {
27
27
  optedIn: string[];
28
28
  }
29
29
  export declare function repairRemovedDefaultTools(cwd: string): RemovedDefaultToolsRepair;
30
+ /**
31
+ * eve 0.65 took `ask_question` out of the default tool set and made it an
32
+ * opt-in workflow tool (`eve add tool/ask_question`). An agent certified on an
33
+ * earlier line had it everywhere, and the notify hooks, the Buzz approval
34
+ * bridge and the Studio question cards all key on that tool name — so every
35
+ * scope that could ask keeps the tool, and a stale `disableTool()` file (a
36
+ * build error from 0.65) is removed.
37
+ */
38
+ export declare const OPT_IN_TOOLS_SINCE: {
39
+ readonly ask_question: "0.65.0";
40
+ };
41
+ export declare function repairOptInTools(cwd: string, eveVersion: string): RemovedDefaultToolsRepair;
@@ -84,3 +84,52 @@ export function repairRemovedDefaultTools(cwd) {
84
84
  }
85
85
  return { removed, optedIn };
86
86
  }
87
+ /**
88
+ * eve 0.65 took `ask_question` out of the default tool set and made it an
89
+ * opt-in workflow tool (`eve add tool/ask_question`). An agent certified on an
90
+ * earlier line had it everywhere, and the notify hooks, the Buzz approval
91
+ * bridge and the Studio question cards all key on that tool name — so every
92
+ * scope that could ask keeps the tool, and a stale `disableTool()` file (a
93
+ * build error from 0.65) is removed.
94
+ */
95
+ export const OPT_IN_TOOLS_SINCE = { ask_question: "0.65.0" };
96
+ function askQuestionSource() {
97
+ return [
98
+ "// eve 0.65 made ask_question an opt-in tool. This agent asked people questions",
99
+ "// on the certified earlier line, so it keeps the tool (kyb upgrade wrote this).",
100
+ 'import { askQuestion } from "eve/tools/ask_question";',
101
+ "",
102
+ "export default askQuestion();",
103
+ "",
104
+ ].join("\n");
105
+ }
106
+ export function repairOptInTools(cwd, eveVersion) {
107
+ const removed = [];
108
+ const optedIn = [];
109
+ if (versionAtLeast(eveVersion, OPT_IN_TOOLS_SINCE.ask_question)) {
110
+ for (const scope of agentScopes(cwd)) {
111
+ const tools = join(scope, "tools");
112
+ const file = join(tools, "ask_question.ts");
113
+ if (existsSync(file)) {
114
+ if (isDisableToolFile(file)) {
115
+ unlinkSync(file);
116
+ removed.push(relative(cwd, file));
117
+ }
118
+ continue;
119
+ }
120
+ mkdirSync(tools, { recursive: true });
121
+ writeFileSync(file, askQuestionSource());
122
+ optedIn.push(relative(cwd, file));
123
+ }
124
+ }
125
+ return { removed, optedIn };
126
+ }
127
+ function versionAtLeast(version, floor) {
128
+ const parse = (v) => v.replace(/^[^\d]*/, "").split("-")[0].split(".").map((n) => Number(n) || 0);
129
+ const a = parse(version), b = parse(floor);
130
+ for (let i = 0; i < 3; i++) {
131
+ if ((a[i] ?? 0) !== (b[i] ?? 0))
132
+ return (a[i] ?? 0) > (b[i] ?? 0);
133
+ }
134
+ return true;
135
+ }
@@ -96,3 +96,21 @@ export declare function notifyMountTs(): string;
96
96
  export declare function reasoningSandboxLibTs(): string;
97
97
  /** What each reasoning-only subagent's `sandbox.ts` re-exports. */
98
98
  export declare function reasoningSandboxReexportTs(): string;
99
+ export interface ComputerPlan {
100
+ files: Array<{
101
+ path: string;
102
+ content: string;
103
+ }>;
104
+ deps: string[];
105
+ steps: string[];
106
+ /** Lines appended to .env.example. */
107
+ env: string;
108
+ }
109
+ /**
110
+ * A persistent Docker desktop with Chrome that the person can watch and take
111
+ * over, as the agent's ROOT sandbox, plus the sighted computer tool, the
112
+ * guarded open_browser, and the surface ladder in instructions. The engineer
113
+ * workshop (a subagent sandbox) is untouched. Needs Docker, so it is an exe
114
+ * host feature: a Vercel function has no daemon to keep a container alive.
115
+ */
116
+ export declare function computerPlan(name: string): ComputerPlan;
package/dist/templates.js CHANGED
@@ -456,28 +456,26 @@ export function hostSteps(host, name, model) {
456
456
  function workshopSandbox(host) {
457
457
  if (host === "exe") {
458
458
  return `import { defineSandbox } from "eve/sandbox";
459
- import { docker } from "eve/sandbox/docker";
459
+ import { DockerSandbox } from "eve/sandbox/docker";
460
460
 
461
461
  /**
462
462
  * The engineer workshop, self-hosted: pnpm + Playwright + Chromium baked into
463
- * the TEMPLATE so warm sessions run render→screenshot→vision in seconds.
463
+ * the ENVIRONMENT (prepared once at build, inherited by every session) so warm
464
+ * sessions run render→screenshot→vision in seconds.
464
465
  *
465
- * Docker rather than vercel(): the hosted backend needs Vercel OIDC, which does
466
- * not exist off-Vercel.
466
+ * Docker rather than Vercel Sandbox: the hosted provider needs Vercel OIDC,
467
+ * which does not exist off-Vercel.
467
468
  *
468
469
  * HOST PREREQUISITE: some images ship Docker disabled (exe.dev's exeuntu runs
469
470
  * \`systemctl disable docker.service\`). Run \`sudo systemctl enable --now docker\`
470
471
  * or every build fails with SandboxTemplateNotProvisionedError.
471
472
  *
472
473
  * NOTE: Docker sessions do not enforce a domain allowlist the way the hosted
473
- * backend does. Egress control is the HOST's responsibility here — a deliberate
474
+ * provider does. Egress control is the HOST's responsibility here — a deliberate
474
475
  * difference from the Vercel deployment, not an oversight.
475
476
  */
476
- export default defineSandbox({
477
- backend: docker(),
478
- revalidationKey: () => "kybernesis-workshop-v5-docker",
479
- async bootstrap({ use }) {
480
- const sandbox = await use();
477
+ export const environment = DockerSandbox.environment({
478
+ prepare: async (sandbox) => {
481
479
  // Base image is Debian-family; refresh indexes before installing browser deps.
482
480
  await sandbox.run({ command: "apt-get update" });
483
481
  await sandbox.run({ command: "npm install -g pnpm" });
@@ -492,28 +490,49 @@ export default defineSandbox({
492
490
  });
493
491
  },
494
492
  });
493
+
494
+ export default defineSandbox(() => environment.open());
495
495
  `;
496
496
  }
497
497
  return `import { defineSandbox } from "eve/sandbox";
498
- import { vercel } from "eve/sandbox/vercel";
498
+ import { VercelSandbox } from "eve/sandbox/vercel";
499
499
 
500
500
  /**
501
501
  * The engineer workshop: a warm, safe cloud dev machine.
502
502
  *
503
- * TEMPLATE bootstrap (once, inherited by every session): pnpm + Playwright +
504
- * Chromium. Prewarm runs at deploy time, so a broken bootstrap fails the build
505
- * loudly and warm sessions run the full render→screenshot→vision loop in
506
- * seconds. Backend PINNED to Vercel Sandbox — hosted sandboxes even from local
507
- * dev (run \`vercel link\` + \`vercel env pull\` first), so evals exercise the
508
- * exact production backend. No Docker anywhere.
503
+ * ENVIRONMENT preparation (once at build, inherited by every session): pnpm +
504
+ * Playwright + Chromium. Preparation runs at deploy time, so a broken recipe
505
+ * fails the build loudly and warm sessions run the full render→screenshot→
506
+ * vision loop in seconds. Provider PINNED to Vercel Sandbox — hosted sandboxes
507
+ * even from local dev (run \`vercel link\` + \`vercel env pull\` first), so evals
508
+ * exercise the exact production provider. No Docker anywhere.
509
509
  *
510
510
  * All sessions run under a domain ALLOWLIST: an agent that installs arbitrary
511
511
  * npm packages must not have open egress. A blocked domain fails loudly;
512
512
  * treat every addition as a security decision.
513
513
  */
514
- export default defineSandbox({
515
- backend: vercel({
516
- resources: { vcpus: 4 },
514
+ export const environment = VercelSandbox.environment({
515
+ resources: { vcpus: 4 },
516
+ prepare: async (sandbox) => {
517
+ // The egress proxy carries HTTPS only; apt defaults to http:// mirrors, so
518
+ // every index fetch silently fails. Rewrite to https first.
519
+ await sandbox.run({
520
+ command:
521
+ "find /etc/apt -type f \\\\( -name '*.list' -o -name '*.sources' \\\\) -exec sed -i 's|http://|https://|g' {} + && apt-get update",
522
+ });
523
+ await sandbox.run({ command: "npm install -g pnpm" });
524
+ await sandbox.run({
525
+ command:
526
+ "mkdir -p /workspace/.shot && cd /workspace/.shot && echo '{\\"name\\":\\"kyb-shot\\",\\"private\\":true}' > package.json && npm install playwright",
527
+ });
528
+ await sandbox.run({
529
+ command: "cd /workspace/.shot && npx playwright install --with-deps chromium",
530
+ });
531
+ },
532
+ });
533
+
534
+ export default defineSandbox(() =>
535
+ environment.open({
517
536
  networkPolicy: {
518
537
  allow: [
519
538
  "registry.npmjs.org",
@@ -541,25 +560,7 @@ export default defineSandbox({
541
560
  ],
542
561
  },
543
562
  }),
544
- revalidationKey: () => "kybernesis-workshop-v5",
545
- async bootstrap({ use }) {
546
- const sandbox = await use();
547
- // The egress proxy carries HTTPS only; apt defaults to http:// mirrors, so
548
- // every index fetch silently fails. Rewrite to https first.
549
- await sandbox.run({
550
- command:
551
- "find /etc/apt -type f \\\\( -name '*.list' -o -name '*.sources' \\\\) -exec sed -i 's|http://|https://|g' {} + && apt-get update",
552
- });
553
- await sandbox.run({ command: "npm install -g pnpm" });
554
- await sandbox.run({
555
- command:
556
- "mkdir -p /workspace/.shot && cd /workspace/.shot && echo '{\\"name\\":\\"kyb-shot\\",\\"private\\":true}' > package.json && npm install playwright",
557
- });
558
- await sandbox.run({
559
- command: "cd /workspace/.shot && npx playwright install --with-deps chromium",
560
- });
561
- },
562
- });
563
+ );
563
564
  `;
564
565
  }
565
566
  export function engineerPlan(host, model) {
@@ -671,7 +672,8 @@ export default defineEvalConfig({
671
672
  // Verified against the exe integration rather than assumed: it validates model
672
673
  // names and refuses one it does not carry, so a stale default here fails the
673
674
  // whole suite at the judge rather than at anything the agent did.
674
- judge: { model: exe(process.env.EXE_JUDGE_MODEL ?? "claude-sonnet-5") },
675
+ // eve ≥0.62 grades with an EVALUATION model, not a language model instance.
676
+ judge: { model: exe.evaluationModel(process.env.EXE_JUDGE_MODEL ?? "claude-sonnet-5") },
675
677
  // Real model and real memory on every turn: generous timeout, gentle concurrency.
676
678
  timeoutMs: 300_000,
677
679
  maxConcurrency: 1,
@@ -748,19 +750,120 @@ export { default } from "@kybernesis/notify/extension";
748
750
  */
749
751
  export function reasoningSandboxLibTs() {
750
752
  return `import { defineSandbox } from "eve/sandbox";
751
- import { justbash } from "eve/sandbox/just-bash";
753
+ import { JustBashSandbox } from "eve/sandbox/just-bash";
752
754
 
753
755
  // The sandbox for a reasoning-only specialist: an instant virtual filesystem,
754
756
  // no Docker container. See agent/subagents/<id>/sandbox.ts, which re-export
755
- // this. Only subagents that do real build/git/network work keep a docker()
757
+ // this. Only subagents that do real build/git/network work keep a Docker
756
758
  // sandbox of their own.
757
- export default defineSandbox({ backend: justbash() });
759
+ export const environment = JustBashSandbox.environment();
760
+
761
+ export default defineSandbox(() => environment.open());
758
762
  `;
759
763
  }
760
764
  /** What each reasoning-only subagent's `sandbox.ts` re-exports. */
761
765
  export function reasoningSandboxReexportTs() {
762
766
  return `// Reasoning-only specialist: no real filesystem, so no Docker container.
763
767
  // See agent/lib/reasoning-sandbox for why this exists and what it replaces.
764
- export { default } from "../../lib/reasoning-sandbox";
768
+ export { default, environment } from "../../lib/reasoning-sandbox";
765
769
  `;
766
770
  }
771
+ /**
772
+ * A persistent Docker desktop with Chrome that the person can watch and take
773
+ * over, as the agent's ROOT sandbox, plus the sighted computer tool, the
774
+ * guarded open_browser, and the surface ladder in instructions. The engineer
775
+ * workshop (a subagent sandbox) is untouched. Needs Docker, so it is an exe
776
+ * host feature: a Vercel function has no daemon to keep a container alive.
777
+ */
778
+ export function computerPlan(name) {
779
+ const files = [
780
+ {
781
+ path: "agent/sandbox.ts",
782
+ content: `import { defineSandbox } from "eve/sandbox";
783
+ import { DockerComputer, prepareComputer, startComputer } from "@kybernesis/computer";
784
+
785
+ // The agent's own computer: ONE persistent container (home and /workspace are
786
+ // volumes, so Chrome's logins survive sessions) with its screen shared over
787
+ // VNC/noVNC on this host's loopback :6080 — reachable only through exe.dev's
788
+ // signed-in port proxy or an SSH tunnel. A person watching can take over.
789
+ // This is the ROOT sandbox; the builder subagent keeps its own workshop.
790
+ export const environment = DockerComputer.environment({
791
+ name: process.env.COMPUTER_NAME ?? "${name}-computer",
792
+ vncPassword: process.env.COMPUTER_VNC_PASSWORD,
793
+ prepare: prepareComputer, // eve's desktop stack + Chrome + the launcher bar, once at \`eve build\`
794
+ });
795
+
796
+ export default defineSandbox(async () => {
797
+ const sandbox = await environment.open();
798
+ await startComputer(sandbox);
799
+ return sandbox;
800
+ });
801
+ `,
802
+ },
803
+ {
804
+ path: "agent/tools/computer.ts",
805
+ content: `import { computerTool } from "@kybernesis/computer";
806
+
807
+ // eve's computer_use with eyes: every action returns the screenshot as an
808
+ // image the model can see, not just a path.
809
+ export default computerTool();
810
+ `,
811
+ },
812
+ {
813
+ path: "agent/tools/open_browser.ts",
814
+ content: `import { openBrowserTool } from "@kybernesis/computer";
815
+
816
+ // Open a site in the computer's Chrome. Guarded: a site that has a connector
817
+ // parks for the person's approval unless the call states a legitimate
818
+ // override — connector first, browser only when there isn't one.
819
+ export default openBrowserTool();
820
+ `,
821
+ },
822
+ {
823
+ path: "agent/tools/close_tabs.ts",
824
+ content: `import { closeTabsTool } from "@kybernesis/computer";
825
+
826
+ // The computer shares memory with the agent host; stale tabs are the spender.
827
+ export default closeTabsTool();
828
+ `,
829
+ },
830
+ {
831
+ path: "agent/instructions/computer.ts",
832
+ content: `import { defineInstructions } from "eve/instructions";
833
+ import { COMPUTER_INSTRUCTIONS } from "@kybernesis/computer";
834
+
835
+ // The "which surface" rule: connected app, then a tool, then the agent's
836
+ // browser, then the person's machine. One source for every agent.
837
+ export default defineInstructions({ content: COMPUTER_INSTRUCTIONS });
838
+ `,
839
+ },
840
+ {
841
+ path: "evals/computer.eval.ts",
842
+ content: `import { computerSuite } from "@kybernesis/evals";
843
+
844
+ // Screen reading, file and cookie persistence across sessions, and the guard:
845
+ // a Gmail question is not answered by browsing mail.google.com. Add
846
+ // \`connected\` / \`disconnected\` once this agent has connectors to prove the
847
+ // order on — see the computerSuite docs.
848
+ export default computerSuite();
849
+ `,
850
+ },
851
+ ];
852
+ return {
853
+ files,
854
+ deps: ["@kybernesis/computer"],
855
+ steps: [
856
+ "Enable Docker on the host (some images ship it disabled): sudo systemctl enable --now docker",
857
+ "Set COMPUTER_VNC_PASSWORD in .env.local (the second gate on the shared screen; exe's port-proxy sign-in is the first)",
858
+ "Watch the screen: https://<vm>.exe.xyz:6080/vnc.html (account-gated), or ssh -N -L 6080:localhost:6080 <vm> and open http://localhost:6080/vnc.html",
859
+ "First `eve build` prepares the computer (eve's desktop stack + Chrome): allow ~5 minutes and ~2 GB of memory for the container",
860
+ ],
861
+ env: `
862
+ # The agent's own computer (@kybernesis/computer): a persistent Docker desktop
863
+ # with Chrome whose screen is shared on this host's loopback :6080 (noVNC),
864
+ # behind exe.dev's port-proxy sign-in. The VNC password is the second gate.
865
+ COMPUTER_NAME="${name}-computer"
866
+ COMPUTER_VNC_PASSWORD="..."
867
+ `,
868
+ };
869
+ }
package/dist/upgrade.js CHANGED
@@ -10,7 +10,7 @@ import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
10
10
  import { newestOnLine } from "./on-line-version.js";
11
11
  import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
12
12
  import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
13
- import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
13
+ import { repairOptInTools, repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
14
14
  import { EVE_VERSION, bold, capture, dim, green, parseEnv, red, run, yellow } from "./util.js";
15
15
  import { inspectEveAgent } from "./agent-limits.js";
16
16
  import { remoteProjectPath, sshTarget } from "./deploy.js";
@@ -332,7 +332,14 @@ function repairRemovedDefaultTools(cwd) {
332
332
  for (const file of optedIn) {
333
333
  console.log(` ${green("+")} wrote ${file} ${dim("(keeps the tool this scope had on eve 0.38)")}`);
334
334
  }
335
- if (removed.length || optedIn.length)
335
+ const optIn = repairOptInTools(cwd, EVE_VERSION);
336
+ for (const file of optIn.removed) {
337
+ console.log(` ${green("-")} removed ${file} ${dim("(disabled a tool eve no longer provides by default; a compile error from eve 0.65)")}`);
338
+ }
339
+ for (const file of optIn.optedIn) {
340
+ console.log(` ${green("+")} wrote ${file} ${dim("(ask_question is opt-in from eve 0.65; this scope keeps it)")}`);
341
+ }
342
+ if (removed.length || optedIn.length || optIn.removed.length || optIn.optedIn.length)
336
343
  console.log();
337
344
  }
338
345
  function repairSandboxCleanupHooks(cwd, deps) {
@@ -578,7 +585,7 @@ function exeJudgeUrl(cwd, env) {
578
585
  if (!path)
579
586
  return null;
580
587
  const source = readFileSync(path, "utf8");
581
- const judgeProvider = /judge\s*:\s*\{[\s\S]*?model\s*:\s*([A-Za-z_$][\w$]*)\s*\(/.exec(source)?.[1];
588
+ const judgeProvider = /judge\s*:\s*\{[\s\S]*?model\s*:\s*([A-Za-z_$][\w$]*)\s*(?:\.evaluationModel\s*)?\(/.exec(source)?.[1];
582
589
  if (!judgeProvider)
583
590
  return null;
584
591
  const escaped = judgeProvider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
package/dist/util.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const EVE_VERSION = "0.51.1";
1
+ export declare const EVE_VERSION = "0.68.0";
2
2
  export declare const REGISTRY_URL = "https://registry.kybernesis.ai/r/{name}.json";
3
3
  export declare const DEFAULT_ISSUER = "https://agent.kybernesis.ai";
4
4
  export declare const green: (s: string) => string;
package/dist/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { createInterface } from "node:readline/promises";
3
- export const EVE_VERSION = "0.51.1";
3
+ export const EVE_VERSION = "0.68.0";
4
4
  export const REGISTRY_URL = "https://registry.kybernesis.ai/r/{name}.json";
5
5
  export const DEFAULT_ISSUER = "https://agent.kybernesis.ai";
6
6
  const TTY = process.stdout.isTTY === true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent \u2014 plus doctor and upgrade.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,8 @@
24
24
  "NOTICE"
25
25
  ],
26
26
  "bin": {
27
- "kyb": "./dist/cli.js"
27
+ "kyb": "./dist/cli.js",
28
+ "kyb-eval": "./dist/eval-cli.js"
28
29
  },
29
30
  "exports": {
30
31
  "./package.json": "./package.json"
@@ -38,7 +39,7 @@
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "24.x",
41
- "eve": "^0.51.1",
42
+ "eve": "0.68.0",
42
43
  "typescript": "7.0.2"
43
44
  },
44
45
  "engines": {
@@ -1989,7 +1989,7 @@ export default defineAgent({
1989
1989
  });
1990
1990
  ```
1991
1991
 
1992
- Four things to know before you promise it:
1992
+ Five things to know before you promise it:
1993
1993
 
1994
1994
  - **The exe LLM integration also serves `anthropic/*` ids, and that is NOT this.**
1995
1995
  It reaches Anthropic through a gateway, which bills metered usage. Same model,
@@ -2000,9 +2000,18 @@ Four things to know before you promise it:
2000
2000
  provider-defined tools, which Anthropic validates by name, and the failure
2001
2001
  (`tools.N.web_search_20250305.name: Input should be 'web_search'`) reads like
2002
2002
  a bug in the agent's own tool definitions.
2003
- - **The sign-in is per host and interactive** — a browser step, once, in the
2004
- container. It survives restarts because it lives in a named volume, but a new
2005
- VM needs its own.
2003
+ - **The sign-in is per host and interactive** — a browser step in the container.
2004
+ It survives restarts because it lives in a named volume, but a new VM needs
2005
+ its own.
2006
+ - **And it is not a one-off: the sign-in expires about every 28 days.** The
2007
+ access token refreshes hourly and does so happily the whole time, which tells
2008
+ you nothing — the refresh token underneath carries a fixed expiry that using
2009
+ it does not extend. On expiry the proxy blanks the credential to empty strings
2010
+ and every turn 401s; only `login` recovers it, never `reload` or a restart.
2011
+ **Say this to the client before you promise the arrangement, and put the
2012
+ re-login in someone's calendar** — it is the operational debt you take on in
2013
+ exchange for not billing metered usage. Run `scripts/claude-subscription.sh
2014
+ status` on a schedule: it now reports days remaining and escalates at 7 and 3.
2006
2015
  - **It is a process to keep alive.** If the container stops, every turn fails
2007
2016
  with a connection error from inside the model SDK, which reads like the model
2008
2017
  being down. `hostPreflight({ claudeProxyUrl })` asks about it at boot; the