@kybernesis/create 0.7.1 → 0.7.2

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/cli.js CHANGED
@@ -6,6 +6,8 @@
6
6
  * self-testing eve agent (also: npm create @kybernesis)
7
7
  * kyb doctor preflight an agent project: keys, issuer, envs, discovery
8
8
  * kyb skills [--global] install/refresh the FDE Claude Code skill suite
9
+ * kyb register register this agent with the control plane (device flow)
10
+ * kyb deploy put this repo on its host and restart it, with proof
9
11
  * kyb upgrade bump @kybernesis/* to latest, gated on the eval suite
10
12
  * --skip-eval skip the eval gate (not for production changes)
11
13
  */
@@ -14,6 +16,8 @@ import { init } from "./init.js";
14
16
  import { doctor } from "./doctor.js";
15
17
  import { upgrade } from "./upgrade.js";
16
18
  import { installSkills } from "./skills.js";
19
+ import { deploy } from "./deploy.js";
20
+ import { register } from "./register.js";
17
21
  function flag(rest, key) {
18
22
  const hit = rest.find((a) => a.startsWith(`--${key}=`));
19
23
  return hit ? hit.slice(key.length + 3) : undefined;
@@ -40,6 +44,12 @@ switch (command) {
40
44
  case "skills":
41
45
  installSkills({ global: rest.includes("--global") });
42
46
  break;
47
+ case "register":
48
+ await register({ name: flag(rest, "name"), url: flag(rest, "url") });
49
+ break;
50
+ case "deploy":
51
+ await deploy({ host: flag(rest, "host") });
52
+ break;
43
53
  case "upgrade":
44
54
  await upgrade(rest.includes("--skip-eval"));
45
55
  break;
@@ -65,6 +75,11 @@ ${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
65
75
  ${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
66
76
  ${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
67
77
  --global ${dim("install to ~/.claude/skills instead of this repo")}
78
+ ${bold("kyb register")} register this agent with the control plane
79
+ --name=<name> ${dim("defaults to KYBERNESIS_AGENT in .env.local")}
80
+ --url=<url> ${dim("defaults to https://$EXE_VM_NAME.exe.xyz")}
81
+ ${bold("kyb deploy")} copy to the host, install, restart, prove it took
82
+ --host=<target> ${dim("ssh target; defaults to $EXE_VM_NAME.exe.xyz")}
68
83
  ${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
69
84
  --skip-eval ${dim("skip the eval gate")}
70
85
 
@@ -0,0 +1,4 @@
1
+ export declare function deploy(options: {
2
+ host?: string;
3
+ dir?: string;
4
+ }): Promise<void>;
package/dist/deploy.js ADDED
@@ -0,0 +1,169 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { bold, dim, green, red, yellow } from "./util.js";
5
+ /**
6
+ * `kyb deploy` — put this repo on its host and restart it, with proof.
7
+ *
8
+ * On Vercel this is `eve deploy` and always was. Off Vercel it was a paragraph
9
+ * in a skill telling people to rsync and then run a script, which is the sort
10
+ * of step that gets done differently by each person doing it — and the
11
+ * differences are exactly where the outages came from.
12
+ */
13
+ function env(dir) {
14
+ const out = {};
15
+ for (const file of [".env.local", ".env"]) {
16
+ const p = join(dir, file);
17
+ if (!existsSync(p))
18
+ continue;
19
+ for (const line of readFileSync(p, "utf8").split("\n")) {
20
+ const m = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line);
21
+ if (m?.[1] && out[m[1]] === undefined)
22
+ out[m[1]] = (m[2] ?? "").trim().replace(/^["']|["']$/g, "");
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+ function hostOf(dir) {
28
+ const pkg = join(dir, "package.json");
29
+ const deps = existsSync(pkg)
30
+ ? (JSON.parse(readFileSync(pkg, "utf8")).dependencies ?? {})
31
+ : {};
32
+ return deps["@kybernesis/exe"] ? "exe" : "vercel";
33
+ }
34
+ /** The ssh target: an explicit flag, then EVE_SSH_HOST, then the exe VM name. */
35
+ function sshTarget(dir, explicit) {
36
+ const e = env(dir);
37
+ if (explicit)
38
+ return explicit;
39
+ if (e.EVE_SSH_HOST)
40
+ return e.EVE_SSH_HOST;
41
+ if (e.EXE_VM_NAME)
42
+ return `${e.EXE_VM_NAME}.exe.xyz`;
43
+ return null;
44
+ }
45
+ export async function deploy(options) {
46
+ const dir = options.dir ?? process.cwd();
47
+ if (!existsSync(join(dir, "agent"))) {
48
+ console.log(red("Not an eve agent project (no agent/ directory)."));
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ console.log(bold("kyb deploy"));
53
+ if (hostOf(dir) === "vercel") {
54
+ console.log(dim(" host: vercel — handing over to eve deploy\n"));
55
+ const r = spawnSync("npx", ["eve", "deploy"], { cwd: dir, stdio: "inherit" });
56
+ process.exitCode = r.status ?? 0;
57
+ return;
58
+ }
59
+ const target = sshTarget(dir, options.host);
60
+ if (!target) {
61
+ console.log(red(" No host to deploy to."));
62
+ console.log(dim(" Set EXE_VM_NAME (or EVE_SSH_HOST) in .env.local, or pass --host=<ssh target>."));
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name ?? "agent";
67
+ const remote = `~/${name}`;
68
+ console.log(dim(` host: ${target} path: ${remote}\n`));
69
+ const run = (cmd, args) => {
70
+ const r = spawnSync(cmd, args, { cwd: dir, stdio: "inherit" });
71
+ return (r.status ?? 1) === 0;
72
+ };
73
+ /**
74
+ * Never copy node_modules, .eve, or a build.
75
+ *
76
+ * node_modules is platform-specific and native modules built on a laptop do
77
+ * not run on the host. `.eve` is the durable store — conversations, turn
78
+ * history, the workflow queue — and overwriting it with a local copy is how
79
+ * a deployment eats its own production state.
80
+ */
81
+ console.log(bold("1/3 Copying source …"));
82
+ const ok = run("rsync", [
83
+ "-az",
84
+ "--delete",
85
+ "--exclude",
86
+ "node_modules",
87
+ "--exclude",
88
+ ".eve",
89
+ "--exclude",
90
+ ".output",
91
+ "--exclude",
92
+ ".git",
93
+ "--exclude",
94
+ ".env.local",
95
+ `${dir}/`,
96
+ `${target}:${remote}/`,
97
+ ]);
98
+ if (!ok) {
99
+ console.log(red(" rsync failed — is the host reachable, and is the path writable?"));
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ console.log(bold("\n2/3 Installing dependencies on the host …"));
104
+ if (!run("ssh", [target, `cd ${remote} && npm install --no-audit --no-fund`])) {
105
+ console.log(red(" npm install failed on the host."));
106
+ process.exitCode = 1;
107
+ return;
108
+ }
109
+ /**
110
+ * Detached, always.
111
+ *
112
+ * `ssh host "script"` sends SIGHUP when the connection ends, which kills the
113
+ * restart halfway — leaving exactly the half-restarted state the script
114
+ * exists to prevent. setsid + nohup + no stdin is the difference between a
115
+ * deploy and an outage.
116
+ */
117
+ console.log(bold("\n3/3 Restarting (detached) …"));
118
+ /**
119
+ * Find the restart script rather than assuming its name.
120
+ *
121
+ * `kyb init` installs scripts/eve-server.sh, but agents predating that have
122
+ * their own — and a deploy that fails on a naming difference is a deploy
123
+ * people stop using. Falls back to installing the packaged script, so a
124
+ * project that has none ends up with the hardened one rather than an error.
125
+ */
126
+ const remoteScript = [
127
+ 'SCRIPT=""',
128
+ // A loop, not a chain of `[ -f x ] && echo x || …` — that keeps evaluating
129
+ // after the first hit and yields every match, so SCRIPT becomes three
130
+ // filenames and `bash "$SCRIPT"` fails on a name nothing has.
131
+ "for f in scripts/eve-server.sh scripts/restart.sh eve-server.sh restart.sh; do",
132
+ ' if [ -f "$f" ]; then SCRIPT="$f"; break; fi',
133
+ "done",
134
+ 'if [ -z "$SCRIPT" ] && [ -f node_modules/@kybernesis/exe/scripts/eve-server.sh ]; then',
135
+ " mkdir -p scripts",
136
+ " cp node_modules/@kybernesis/exe/scripts/eve-server.sh scripts/",
137
+ " chmod +x scripts/eve-server.sh",
138
+ ' SCRIPT="scripts/eve-server.sh"',
139
+ "fi",
140
+ 'if [ -z "$SCRIPT" ]; then echo "FAILED: no restart script on the host"; exit 1; fi',
141
+ 'echo "using $SCRIPT"',
142
+ 'setsid nohup bash "$SCRIPT" > /tmp/kyb-deploy.log 2>&1 < /dev/null &',
143
+ "sleep 2",
144
+ "echo started",
145
+ ].join("\n");
146
+ run("ssh", [target, `cd ${remote}\n${remoteScript}`]);
147
+ console.log(dim("\n Waiting for the restart to report …"));
148
+ let last = "";
149
+ for (let i = 0; i < 40; i++) {
150
+ const out = execFileSync("ssh", [target, `tail -6 /tmp/kyb-deploy.log 2>/dev/null || true`], {
151
+ encoding: "utf8",
152
+ });
153
+ last = out;
154
+ if (/health:|FAILED/.test(out))
155
+ break;
156
+ await new Promise((r) => setTimeout(r, 5000));
157
+ }
158
+ const healthy = /health:\s*200/.test(last);
159
+ console.log(last
160
+ .split("\n")
161
+ .filter((l) => /pid=|build:|OK:|health:|FAILED|SOURCE IS NEWER|^built/.test(l))
162
+ .map((l) => ` ${l}`)
163
+ .join("\n"));
164
+ console.log(healthy
165
+ ? green("\n ✓ deployed and serving the current build")
166
+ : yellow("\n ! the restart did not report health — check /tmp/kyb-deploy.log on the host"));
167
+ if (!healthy)
168
+ process.exitCode = 1;
169
+ }
@@ -0,0 +1,5 @@
1
+ export declare function register(options: {
2
+ name?: string;
3
+ url?: string;
4
+ dir?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { bold, dim, green, red, yellow } from "./util.js";
4
+ function envOf(dir) {
5
+ const out = {};
6
+ for (const file of [".env.local", ".env"]) {
7
+ const p = join(dir, file);
8
+ if (!existsSync(p))
9
+ continue;
10
+ for (const line of readFileSync(p, "utf8").split("\n")) {
11
+ const m = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line);
12
+ if (m?.[1] && out[m[1]] === undefined)
13
+ out[m[1]] = (m[2] ?? "").trim().replace(/^["']|["']$/g, "");
14
+ }
15
+ }
16
+ return out;
17
+ }
18
+ /** Sign in with the device flow and return an identity token. */
19
+ async function signIn(issuer) {
20
+ const started = await fetch(`${issuer}/api/oauth/device`, {
21
+ method: "POST",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify({ deviceId: `kyb-cli-${process.pid}`, deviceLabel: "kyb CLI" }),
24
+ }).catch(() => null);
25
+ if (!started?.ok) {
26
+ console.log(red(` Could not start sign-in at ${issuer}.`));
27
+ return null;
28
+ }
29
+ const body = (await started.json());
30
+ const url = String(body.verification_uri_complete ?? body.verification_uri ?? "");
31
+ const code = String(body.user_code ?? "");
32
+ const deviceCode = String(body.device_code ?? "");
33
+ let interval = Number(body.interval ?? 5) * 1000;
34
+ const deadline = Date.now() + Number(body.expires_in ?? 600) * 1000;
35
+ console.log(`\n Approve this in your browser:\n`);
36
+ console.log(` ${bold(url)}`);
37
+ if (code)
38
+ console.log(` code: ${bold(code)}\n`);
39
+ while (Date.now() < deadline) {
40
+ await new Promise((r) => setTimeout(r, interval));
41
+ const res = await fetch(`${issuer}/api/oauth/token`, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify({ device_code: deviceCode }),
45
+ }).catch(() => null);
46
+ if (!res)
47
+ continue;
48
+ const out = (await res.json().catch(() => ({})));
49
+ if (res.ok && typeof out.token === "string")
50
+ return out.token;
51
+ if (out.error === "slow_down")
52
+ interval += 5000;
53
+ // authorization_pending is the normal case; anything else is fatal.
54
+ if (out.error && out.error !== "authorization_pending" && out.error !== "slow_down") {
55
+ console.log(red(` Sign-in failed: ${String(out.error)}`));
56
+ return null;
57
+ }
58
+ }
59
+ console.log(red(" Sign-in timed out."));
60
+ return null;
61
+ }
62
+ export async function register(options) {
63
+ const dir = options.dir ?? process.cwd();
64
+ const env = envOf(dir);
65
+ const issuer = (env.KYBERNESIS_ISSUER || "https://agent.kybernesis.ai").replace(/\/$/, "");
66
+ // The registered name must equal KYBERNESIS_AGENT exactly — it is what the
67
+ // agent checks grants against, and a mismatch is a 403 with no clue in it.
68
+ const name = options.name ?? env.KYBERNESIS_AGENT;
69
+ const url = options.url ??
70
+ env.EVE_PUBLIC_URL ??
71
+ (env.EXE_VM_NAME ? `https://${env.EXE_VM_NAME}.exe.xyz` : undefined);
72
+ console.log(bold("kyb register"));
73
+ console.log(dim(` issuer: ${issuer}`));
74
+ if (!name) {
75
+ console.log(red(" No agent name. Set KYBERNESIS_AGENT in .env.local, or pass --name=<name>."));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ if (!url) {
80
+ console.log(red(" No deployment URL. Set EXE_VM_NAME in .env.local, or pass --url=<https://…>."));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ console.log(dim(` agent: ${name}\n url: ${url}`));
85
+ const token = await signIn(issuer);
86
+ if (!token) {
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ const res = await fetch(`${issuer}/api/agents/register`, {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
93
+ body: JSON.stringify({ name, url }),
94
+ }).catch(() => null);
95
+ if (!res?.ok) {
96
+ const detail = res ? (await res.json().catch(() => ({}))).error : "unreachable";
97
+ console.log(red(`\n Registration failed: ${detail ?? res?.status}`));
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ const out = (await res.json());
102
+ console.log(green(out.created
103
+ ? `\n ✓ registered "${name}" and granted you access`
104
+ : `\n ✓ "${name}" already existed — its URL now points at ${url}`));
105
+ console.log(dim(" Grant others in the admin; they do not inherit yours."));
106
+ if (!env.KYBERNESIS_AGENT_CREDENTIAL) {
107
+ console.log(yellow("\n ! This agent has no credential yet. Turn on 'Work on this computer' in\n" +
108
+ " KYBER Studio to mint and install one — do not paste a credential by hand."));
109
+ }
110
+ }
package/dist/skills.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { bold, dim, green } from "./util.js";
6
6
  /** The skill suite shipped inside this package (skills/ beside dist/). */
@@ -26,6 +26,18 @@ export function installSkills(opts = {}) {
26
26
  const target = opts.global
27
27
  ? join(homedir(), ".claude", "skills")
28
28
  : join(process.cwd(), ".claude", "skills");
29
+ /**
30
+ * Never install the suite into itself.
31
+ *
32
+ * Run from inside the package's own skills/ directory, this copies the suite
33
+ * to skills/.claude/skills — which then ships inside the published tarball,
34
+ * so every consumer installs a duplicate suite nested one level down. That
35
+ * happened, got committed, and was one `npm publish` from being everyone's.
36
+ */
37
+ if (resolve(target).startsWith(resolve(src))) {
38
+ console.error("Refusing to install the suite into itself — run kyb skills from an agent repo, not from the package.");
39
+ process.exit(2);
40
+ }
29
41
  mkdirSync(target, { recursive: true });
30
42
  const names = readdirSync(src).filter((n) => !n.startsWith("."));
31
43
  for (const name of names) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",
@@ -1804,6 +1804,17 @@ keys of your own.
1804
1804
 
1805
1805
  ### 11.1 Scaffold
1806
1806
 
1807
+ The whole path is commands now — there is no step where an FDE is expected to
1808
+ improvise a deploy:
1809
+
1810
+ ```bash
1811
+ kyb init <name> --host=exe --channel=none --engineer --studio
1812
+ kyb register # device flow; grants you; idempotent by name
1813
+ kyb deploy # copy + install + restart + prove
1814
+ kyb doctor && npm run eval
1815
+ ```
1816
+
1817
+
1807
1818
  ```bash
1808
1819
  kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer
1809
1820
  cd <name> && kyb doctor
@@ -86,9 +86,13 @@ then `eve add @kybernesis/<item>`). Each covers one axis:
86
86
  engineer? })` = smoke + 5 memory + routing per dept + optional vision-loop
87
87
  eval. Judge model ≠ model under test. Hermetic runs force all workspaces to
88
88
  `<name>-eval` via the npm script.
89
- - **create** — the `kyb` CLI: `init [--engineer]`, `doctor`, `upgrade`
90
- (carries eve to the Kybernesis-CERTIFIED pin, never blind latest),
91
- `skills`. Ships THIS skill suite.
89
+ - **create** — the `kyb` CLI, and the whole lifecycle of an agent:
90
+ `init` (scaffold; `--host=exe` also installs the hardened restart script),
91
+ `register` (control plane, via device flow — no admin session, no pasted
92
+ token, grants the person who ran it, idempotent by name),
93
+ `deploy` (copy + install + restart + PROVE it; `eve deploy` on Vercel),
94
+ `doctor`, `upgrade` (carries eve to the Kybernesis-CERTIFIED pin, never
95
+ blind latest), `skills`. Ships THIS skill suite.
92
96
 
93
97
  ## Gotchas that each cost a real debugging session
94
98
 
@@ -15,11 +15,21 @@ the deployment, not a shortcut. It will fail on the real engagement.
15
15
  ## Scaffold
16
16
 
17
17
  ```bash
18
- kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer
18
+ kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer --studio
19
+ kyb register # control plane: device flow, grants you, idempotent by name
20
+ kyb deploy # copy + install + restart, and prove it took
21
+ kyb doctor # preflight; it knows the self-hosted failure modes below
19
22
  ```
20
23
 
21
- `--host=exe` swaps the bindings; everything else is the same product. Run
22
- `kyb doctor` after it knows the self-hosted failure modes below.
24
+ `--host=exe` swaps the bindings and installs `scripts/eve-server.sh` the
25
+ restart script, with every lesson below already in it. `--studio` adds local
26
+ execution and the management routes.
27
+
28
+ **Deploying is `kyb deploy`, not a hand-rolled rsync.** It refuses to copy
29
+ `node_modules` (native modules built on a laptop do not run on the host) or
30
+ `.eve` (the durable store — copying over it eats production state), restarts
31
+ detached so a dropped connection cannot SIGHUP the restart halfway, and waits
32
+ for the script to report health rather than reporting success on exit code.
23
33
 
24
34
  ## What Vercel gives you that a client host does not
25
35
 
@@ -1,75 +0,0 @@
1
- ---
2
- description: Use when running evals, certifying an agent or an eve version bump, debugging eval failures, or preparing a release — the Kybernesis QA discipline and its run hygiene.
3
- ---
4
-
5
- # Certification & eval discipline
6
-
7
- The rule: **evals gate every deploy, and the consuming agent's suite is the
8
- release gate for every package change.** Nothing ships on "it looks right" —
9
- green suite or it doesn't go.
10
-
11
- ## The suite
12
-
13
- `kybernesisBaseline()` from `@kybernesis/evals` in `evals/kybernesis.eval.ts`:
14
- smoke (boots, replies, identifies itself), five memory evals (no memory
15
- thrash on greetings; explicit remember never refused; proactive storage;
16
- brain-note two-step in order; cross-session unprompted recall), one routing
17
- eval per department, and with `engineer: true` the vision-loop eval
18
- (screenshot tool fires and the judge confirms the model SAW the render).
19
- Judge model is configured in `evals/evals.config.ts` and must NEVER be the
20
- model under test.
21
-
22
- ## Run hygiene (each rule ate a real run)
23
-
24
- - `npm run eval` — always through the npm script: it forces every Arcana
25
- workspace to `<name>-eval` so evals never write into a real brain.
26
- - **Kill any running dev server first** (`pkill -f "eve dev"`) — eve eval
27
- attaches to an existing instance and runs stale code.
28
- - **Never edit the repo mid-run** — the dev runtime watches `agent/`; an
29
- edit breaks the rebuild and kills remaining evals.
30
- - Engineer eval: hosted Vercel sandbox (no Docker), needs `vercel link` +
31
- `vercel env pull` (VERCEL_OIDC_TOKEN). Warm template ≈3–4 min; a
32
- pre-first-deploy cold bake is budgeted 20 min.
33
- - Stale sandbox state (migration errors, re-baking templates):
34
- `rm -rf .eve/sandbox-cache .eve/dev-runtime` and rerun.
35
- - Don't pipe the eval command through `tail` in scripts — it masks the exit
36
- code (and `| tail -N` on a backgrounded run destroys the per-eval detail —
37
- `tee` to a file instead).
38
- - **Heavy-model suites: `maxConcurrency: 1` locally.** At 2, long opus turns
39
- overload the local world-queue transport (`Queue delivery failed … fetch
40
- failed`); crashed deliveries REPLAY subagent steps, surfacing as
41
- `lost continuationToken` races and phantom failures that move between runs.
42
- The deployed runtime uses real queue infra — this is a local-harness limit.
43
- - **AI Gateway budget is a silent eval killer**: Vercel applies a default
44
- per-project budget (e.g. $10/daily); a suite of real opus turns can exhaust
45
- it MID-RUN → `MODEL_CALL_FAILED` on whatever ran last. Check/raise:
46
- `vercel ai-gateway budgets list` / `budgets set project <name> --limit 30
47
- --refresh-period monthly`.
48
- - **"run parked on N unanswered input request(s)"** = the agent called a
49
- human-in-the-loop tool (`approval: status=pending tool=ask_question` in the
50
- turn log) — no one answers in an eval. Usually a behavior finding: the
51
- fixture was self-contained and the agent asked instead of acting. Fix the
52
- agent's bias-to-act instructions, not the fixture.
53
-
54
- ## eve version certification
55
-
56
- Clients pin the **Kybernesis-certified** eve version (`kyb upgrade` carries
57
- them there — never blind npm-latest). Certifying a new eve: bump in a branch
58
- → typecheck → `npx eve info` → full suite → live smoke on the deployed
59
- surface → advance the pin in @kybernesis/create → record the certification.
60
-
61
- ## When an eval fails
62
-
63
- Read the eval's transcript before touching fixtures. Order of suspicion:
64
- (1) environment (stale dev server, missing env, cold template), (2) a real
65
- behavior regression — fix the agent, (3) only THEN the fixture — and if a
66
- fixture changes, the reason becomes a comment on it. A failure that reveals
67
- a new failure mode becomes a new fixture: that is how the suite grew every
68
- guard it has.
69
-
70
- ## Release flow (packages)
71
-
72
- Edit in `~/platform` → build → bump → human publishes (browser auth) →
73
- consuming agent bumps → **full suite green** → deploy → registry item update
74
- + deploy if install files changed. Then propagate the lesson (see the
75
- `source-of-truth` skill).
@@ -1,119 +0,0 @@
1
- ---
2
- description: Use when connecting two deployed eve agents so one can delegate to the other — "connect agent A to agent B", agent-to-agent communication, remote peers, cross-deployment delegation. Wires @kybernesis/dispatch edges end to end.
3
- ---
4
-
5
- # Connecting two eve agents (@kybernesis/dispatch)
6
-
7
- An **edge** lets one deployed eve agent call another as if it were a local
8
- subagent, with the human's identity carried across the hop. One edge covers a
9
- full question-and-answer round trip (the caller parks until the peer's callback
10
- returns). Wire the mirror-image edge only if the other agent should also be
11
- able to *initiate*.
12
-
13
- ## Before wiring — gather the facts
14
-
15
- 1. **Both repos' eve versions must be compatible** (`node_modules/eve/package.json`
16
- in each). An old receiver silently drops principal forwarding and runs as
17
- service identity — no error. Upgrade both ends together first if they differ.
18
- 2. **Vercel identities** of both projects: team slug + project name as shown in
19
- `npx vercel ls <project>` (slugs, not `team_…`/`prj_…` IDs).
20
- 3. **Stable production URL** of the callee: `npx vercel inspect <latest-prod-url>`
21
- → Aliases — then **verify the alias is OPEN before wiring it**:
22
- `curl -s -o /dev/null -w "%{http_code}" <url>/eve/v1/health` must return
23
- **200**. The `<project>-<team>.vercel.app` aliases commonly sit behind
24
- Vercel SSO deployment protection (302 → vercel.com/sso-api) and CANNOT
25
- receive dispatches; the shorter production alias is usually the open one.
26
- 4. Both repos need `@kybernesis/dispatch` installed (`npm i @kybernesis/dispatch`).
27
-
28
- ## Caller side — one file
29
-
30
- `agent/subagents/<peer-name>.ts` (file name = tool name the model routes to):
31
-
32
- ```ts
33
- import { remotePeer } from "@kybernesis/dispatch";
34
-
35
- export default remotePeer({
36
- envVar: "GTM_AGENT_URL",
37
- description: "…", // see below — this is the whole routing story
38
- });
39
- ```
40
-
41
- **Write the description from the CALLEE's actual capabilities.** Read the peer
42
- repo's `agent/instructions*`, subagent descriptions, and skills, then write the
43
- concrete topics people ask about ("posting cadence, open GTM plays, outreach
44
- targets, content drafting in the house voice") — not a generic blurb. If the
45
- caller has local subagents with overlapping remits, differentiate explicitly or
46
- routing will be ambiguous.
47
-
48
- Set the env var on the caller's Vercel project:
49
- `printf "<stable-prod-url>" | npx vercel env add GTM_AGENT_URL production`
50
-
51
- ## Receiver side — one file
52
-
53
- `agent/channels/eve.ts` on the callee:
54
-
55
- ```ts
56
- import { dispatchChannel } from "@kybernesis/dispatch";
57
-
58
- export default dispatchChannel({
59
- trustedPeers: [{ teamSlug: "<caller-team>", projectName: "<caller-project>" }],
60
- });
61
- ```
62
-
63
- If the callee already has an authored `agent/channels/eve.ts` with app auth,
64
- either migrate it to `dispatchChannel({ trustedPeers, extraAuth: […] })` or add
65
- the peer by hand to BOTH the `vercelOidc({ subjects })` list and the
66
- `trustedForwarders` predicate — they must never drift apart. Never write
67
- `trustedForwarders: () => true`.
68
-
69
- ## Verify
70
-
71
- 1. `npx eve info` in both repos: 0 diagnostics; the caller's manifest gains a
72
- `remoteAgents` entry (it does NOT appear in the local subagent count).
73
- 2. `npm run typecheck` both.
74
- 3. Deploy BOTH (`npx eve deploy` / git push per repo convention). The edge is
75
- live only when both ends are.
76
- 4. Live test from the caller's real surface (e.g. Slack): ask something only
77
- the peer knows. Confirm delegation in the caller's reply, then check
78
- telemetry (PostHog): the peer-side turn should carry the human's
79
- distinct_id, plus the `eve:forwarded-by` attribute naming the caller.
80
-
81
- ## Failure signatures
82
-
83
- - **403 on dispatch** → receiver has no authored eve channel, or the caller
84
- isn't in `trustedPeers`. Check team slug/project name spelling — a typo
85
- silently rejects everything.
86
- - **`principal_required` on the peer's user-scoped connections** → forwarding
87
- isn't arriving: receiver predates forwarding, or the assertion was dropped.
88
- - **Peer never gets called** → routing description too vague, or it collides
89
- with a local subagent's remit. Rewrite from the callee's real capabilities.
90
- - **Works locally, 401 in production** → caller's OIDC not accepted: the
91
- receiver's `trustedPeers` names the wrong environment (default is
92
- production-only) or wrong project.
93
-
94
- ## Governed mode (control-plane edges — preferred when the client runs the admin)
95
-
96
- Instead of hand-enumerated peers, edges are GRANTED in the Kybernesis control
97
- plane and enforced with 300s A2A tokens. Full lifecycle proven live 2026-08-07
98
- (grant → dispatch → revoke → refused ≤5 min → re-grant → restored, no deploys).
99
-
100
- Setup, per agent (admin UI /agents):
101
- 1. Register both agents as eve deployments — URL must be the OPEN production
102
- alias (health 200; forms sanitize pasted punctuation as of this writing).
103
- 2. Grant the edge on the CALLEE's panel ("Agent-to-agent edges" → allow calls
104
- from <caller> + purpose; expiry optional — self-destructing edge).
105
- 3. Mint each agent's credential (shown ONCE) → set as Sensitive env
106
- `KYBERNESIS_AGENT_CREDENTIAL` on that agent's Vercel project.
107
-
108
- Then in code (dispatch ≥0.2.1 + enterprise ≥0.2.0 both installed):
109
- - caller: `remotePeer({ callee: "<EXACT registered name>", governed: { issuer },
110
- envVar, fallbackUrl, description })` — envVar/fallbackUrl kept as overrides;
111
- registry supplies the URL when the credential is present.
112
- - receiver: `dispatchChannel({ governed: { issuer, agent: "<own name>" } })`.
113
-
114
- Gotchas: names are case-sensitive ("Kyber" ≠ "kyber" — copy from the admin);
115
- credentials are one JWS line ~3 segments, an ES256 signature is 86 base64url
116
- chars — a truncated paste fails verification silently, so length-check it;
117
- scheduled/cron turns still forward no human (service identity at the peer);
118
- the deployed agent's model spend shares the project's AI Gateway budget with
119
- local eval runs — size the budget for BOTH or production goes model-dead.