@kybernesis/create 0.3.2 → 0.5.0
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 +22 -4
- package/dist/doctor.js +97 -17
- package/dist/init.d.ts +12 -2
- package/dist/init.js +116 -66
- package/dist/templates.d.ts +29 -1
- package/dist/templates.js +358 -5
- package/package.json +1 -1
- package/skills/fde-engagement/references/playbook.md +15 -0
- package/skills/self-hosting/SKILL.md +96 -0
package/dist/cli.js
CHANGED
|
@@ -14,10 +14,24 @@ import { init } from "./init.js";
|
|
|
14
14
|
import { doctor } from "./doctor.js";
|
|
15
15
|
import { upgrade } from "./upgrade.js";
|
|
16
16
|
import { installSkills } from "./skills.js";
|
|
17
|
+
function flag(rest, key) {
|
|
18
|
+
const hit = rest.find((a) => a.startsWith(`--${key}=`));
|
|
19
|
+
return hit ? hit.slice(key.length + 3) : undefined;
|
|
20
|
+
}
|
|
21
|
+
function initOptions(rest) {
|
|
22
|
+
const subs = flag(rest, 'subagents');
|
|
23
|
+
return {
|
|
24
|
+
engineer: rest.includes('--engineer'),
|
|
25
|
+
channel: flag(rest, "channel"),
|
|
26
|
+
host: flag(rest, "host"),
|
|
27
|
+
subagents: subs === undefined ? undefined : subs.split(',').map((s) => s.trim()).filter(Boolean),
|
|
28
|
+
yes: rest.includes('--yes') || rest.includes('-y'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
17
31
|
const [, , command, ...rest] = process.argv;
|
|
18
32
|
switch (command) {
|
|
19
33
|
case "init":
|
|
20
|
-
await init(rest.find((a) => !a.startsWith("-")),
|
|
34
|
+
await init(rest.find((a) => !a.startsWith("-")), initOptions(rest));
|
|
21
35
|
break;
|
|
22
36
|
case "doctor":
|
|
23
37
|
await doctor();
|
|
@@ -29,19 +43,23 @@ switch (command) {
|
|
|
29
43
|
await upgrade(rest.includes("--skip-eval"));
|
|
30
44
|
break;
|
|
31
45
|
case undefined:
|
|
32
|
-
await init(undefined,
|
|
46
|
+
await init(undefined, initOptions(rest));
|
|
33
47
|
break;
|
|
34
48
|
default:
|
|
35
49
|
if (!command.startsWith("-")) {
|
|
36
50
|
// `npm create @kybernesis acme-agent` → argv[2] is the name.
|
|
37
|
-
await init(command,
|
|
51
|
+
await init(command, initOptions(rest));
|
|
38
52
|
break;
|
|
39
53
|
}
|
|
40
54
|
console.log(`
|
|
41
55
|
${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
|
|
42
56
|
|
|
43
|
-
${bold("kyb init [name]")} scaffold a
|
|
57
|
+
${bold("kyb init [name]")} scaffold a Kybernesis eve agent (core: enterprise + arcana + evals)
|
|
58
|
+
--channel=<kind> ${dim("none|slack|imessage|telegram|discord|web (default: none)")}
|
|
59
|
+
--host=<kind> ${dim("vercel|exe (default: vercel)")}
|
|
60
|
+
--subagents=a,b ${dim("department subagents (default: none)")}
|
|
44
61
|
--engineer ${dim("add the engineer layer: workshop sandbox + vision dev loop")}
|
|
62
|
+
--yes ${dim("no prompts; take flags and defaults")}
|
|
45
63
|
${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
|
|
46
64
|
${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
|
|
47
65
|
--global ${dim("install to ~/.claude/skills instead of this repo")}
|
package/dist/doctor.js
CHANGED
|
@@ -90,24 +90,39 @@ export async function doctor() {
|
|
|
90
90
|
add("warn", "KYBERNESIS_ISSUER not set", "agent is not control-plane governed");
|
|
91
91
|
}
|
|
92
92
|
// ── slack ──────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// Only relevant when the agent actually has a Slack channel — a client on
|
|
94
|
+
// iMessage or Telegram should never be told to create a Slack connector.
|
|
95
|
+
const hasSlackChannel = existsSync(join(cwd, "agent/channels/slack.ts"));
|
|
96
|
+
if (hasSlackChannel) {
|
|
97
|
+
if (env.SLACK_CONNECTOR_UID)
|
|
98
|
+
add("pass", `Slack connector uid: ${env.SLACK_CONNECTOR_UID}`, "verify trigger path /eve/v1/slack (vercel connect list)");
|
|
99
|
+
else if (env.SLACK_BOT_TOKEN)
|
|
100
|
+
add("pass", "Slack via portable credentials (SLACK_BOT_TOKEN)");
|
|
101
|
+
else
|
|
102
|
+
add("warn", "Slack channel present but no credentials", "SLACK_CONNECTOR_UID (Vercel) or SLACK_BOT_TOKEN (portable)");
|
|
103
|
+
}
|
|
97
104
|
// ── engineer layer (optional — checked only when installed) ────────────
|
|
98
105
|
const hasEngineer = Boolean(deps["@kybernesis/engineer"]) || existsSync(join(cwd, "agent/extensions/engineer.ts"));
|
|
99
106
|
if (hasEngineer) {
|
|
100
107
|
add("pass", `@kybernesis/engineer ${deps["@kybernesis/engineer"] ?? "(extension file present)"}`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
add("pass",
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// The workshop may sit on the root OR on the engineer subagent (the
|
|
109
|
+
// scoped pattern). Either is valid; neither is not.
|
|
110
|
+
const rootSandbox = existsSync(join(cwd, "agent/sandbox/sandbox.ts"));
|
|
111
|
+
const builderSandbox = existsSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts"));
|
|
112
|
+
if (rootSandbox || builderSandbox) {
|
|
113
|
+
add("pass", `workshop sandbox present (${builderSandbox ? "engineer subagent" : "root agent"})`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
add("fail", "no workshop sandbox", "the engineer layer needs one — on the root or on agent/subagents/builder/");
|
|
117
|
+
}
|
|
109
118
|
const vercelConn = join(cwd, "agent/connections/vercel.ts");
|
|
110
|
-
|
|
119
|
+
const selfHostedAgent = Boolean(deps["@kybernesis/exe"]) ||
|
|
120
|
+
(existsSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts")) &&
|
|
121
|
+
readFileSync(join(cwd, "agent/subagents/builder/sandbox/sandbox.ts"), "utf8").includes("docker("));
|
|
122
|
+
if (selfHostedAgent && !existsSync(vercelConn)) {
|
|
123
|
+
add("pass", "no Vercel MCP connection (self-hosted)", "public deploys need the CLIENT's own Vercel token — Vercel Connect does not work off-Vercel");
|
|
124
|
+
}
|
|
125
|
+
else if (existsSync(vercelConn)) {
|
|
111
126
|
const src = readFileSync(vercelConn, "utf8");
|
|
112
127
|
const uid = /connect\(\s*"([^"]+)"/.exec(src)?.[1];
|
|
113
128
|
if (uid && uid.includes("/"))
|
|
@@ -118,10 +133,12 @@ export async function doctor() {
|
|
|
118
133
|
else {
|
|
119
134
|
add("warn", "agent/connections/vercel.ts missing — no preview deploys/link-back", "eve add connection/vercel, then vercel connect create + attach");
|
|
120
135
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
if (!selfHostedAgent) {
|
|
137
|
+
if (env.VERCEL_OIDC_TOKEN || env.VERCEL_TOKEN)
|
|
138
|
+
add("pass", "Vercel credentials for local hosted sandboxes");
|
|
139
|
+
else
|
|
140
|
+
add("warn", "no VERCEL_OIDC_TOKEN — local sandbox/eval runs cannot reach Vercel Sandbox", "vercel link && vercel env pull");
|
|
141
|
+
}
|
|
125
142
|
}
|
|
126
143
|
// ── dispatch edges (agent-to-agent — checked only when present) ────────
|
|
127
144
|
const subagentsDir = join(cwd, "agent/subagents");
|
|
@@ -168,6 +185,69 @@ export async function doctor() {
|
|
|
168
185
|
add("warn", "@kybernesis/dispatch installed but no edges or dispatch channel found", "see the connect-agents skill");
|
|
169
186
|
}
|
|
170
187
|
}
|
|
188
|
+
// ── self-hosted agents (host !== Vercel) ───────────────────────────────
|
|
189
|
+
// Every check here cost a real debugging session on the first exe.dev
|
|
190
|
+
// deployment. None of them are theoretical.
|
|
191
|
+
const selfHosted = Boolean(deps["@kybernesis/exe"]) ||
|
|
192
|
+
existsSync(join(cwd, "agent/sandbox/sandbox.ts")) &&
|
|
193
|
+
readFileSync(join(cwd, "agent/sandbox/sandbox.ts"), "utf8").includes("docker(");
|
|
194
|
+
if (selfHosted) {
|
|
195
|
+
// Vercel Connect needs Vercel OIDC — it CANNOT work off-Vercel, for Slack,
|
|
196
|
+
// the Vercel MCP connection, or anything else. Every such connection has to
|
|
197
|
+
// become a static credential the client issues.
|
|
198
|
+
const connectUsers = [];
|
|
199
|
+
for (const dir of ["agent/channels", "agent/connections"]) {
|
|
200
|
+
const full = join(cwd, dir);
|
|
201
|
+
if (!existsSync(full))
|
|
202
|
+
continue;
|
|
203
|
+
for (const f of readdirSync(full)) {
|
|
204
|
+
const file = join(full, f);
|
|
205
|
+
if (!f.endsWith(".ts"))
|
|
206
|
+
continue;
|
|
207
|
+
if (readFileSync(file, "utf8").includes("@vercel/connect"))
|
|
208
|
+
connectUsers.push(`${dir}/${f}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (connectUsers.length) {
|
|
212
|
+
add("fail", `Vercel Connect used off-Vercel: ${connectUsers.join(", ")}`, "Connect authenticates via Vercel OIDC, which does not exist on this host — the agent will fail to boot. Switch to portable/static credentials");
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
add("pass", "no Vercel Connect dependencies (correct for a self-hosted agent)");
|
|
216
|
+
}
|
|
217
|
+
// eve start does not read .env.local the way eve dev does.
|
|
218
|
+
add("warn", "self-hosted: export .env.local into the server process", "eve start does NOT read it; use the supervision script from @kybernesis/exe (scripts/eve-server.sh)");
|
|
219
|
+
// Prewarm runs in the eve CLI, not the built server.
|
|
220
|
+
add("warn", "self-hosted: start via `npx eve start`, not `node .output/server/index.mjs`", "sandbox templates are prewarmed by the CLI; starting the server directly skips prewarm and every sandbox tool fails with SandboxTemplateNotProvisionedError");
|
|
221
|
+
}
|
|
222
|
+
// ── engineer subagent (build capability scoped to a subagent) ──────────
|
|
223
|
+
const builderDir = join(cwd, "agent/subagents/builder");
|
|
224
|
+
if (existsSync(builderDir)) {
|
|
225
|
+
// Subagents own their sandbox — they do NOT inherit the root's. Without one
|
|
226
|
+
// the builder gets a bare template and every screenshot fails with
|
|
227
|
+
// "Cannot find module 'playwright'" while the root's template is fine.
|
|
228
|
+
if (existsSync(join(builderDir, "sandbox/sandbox.ts"))) {
|
|
229
|
+
add("pass", "engineer subagent has its own workshop sandbox");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
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");
|
|
233
|
+
}
|
|
234
|
+
if (existsSync(join(builderDir, "extensions/engineer.ts"))) {
|
|
235
|
+
add("pass", "engineer mounted locally on the subagent (root keeps no shell)");
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
add("warn", "engineer extension not mounted on the subagent", "agent/subagents/builder/extensions/engineer.ts");
|
|
239
|
+
}
|
|
240
|
+
// Delivery: either storage works, or the agent cannot hand over artifacts.
|
|
241
|
+
if (env.BLOB_READ_WRITE_TOKEN) {
|
|
242
|
+
add("pass", "file delivery via Vercel Blob");
|
|
243
|
+
}
|
|
244
|
+
else if (env.DELIVER_DIR && env.DELIVER_BASE_URL) {
|
|
245
|
+
add("pass", `file delivery via host directory (${env.DELIVER_DIR})`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
add("warn", "file delivery not configured — the agent cannot hand over artifacts", "set BLOB_READ_WRITE_TOKEN (the CLIENT's blob store) or DELIVER_DIR + DELIVER_BASE_URL");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
171
251
|
// ── eve discovery + local port ─────────────────────────────────────────
|
|
172
252
|
const info = capture("npx", ["eve", "info"], cwd);
|
|
173
253
|
if (info === null)
|
package/dist/init.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ChannelKind, type HostKind } from "./templates.js";
|
|
2
|
+
export interface InitOptions {
|
|
2
3
|
engineer?: boolean;
|
|
3
|
-
|
|
4
|
+
/** Chat surface. Default "none" — add later with `kyb add channel`. */
|
|
5
|
+
channel?: ChannelKind;
|
|
6
|
+
/** Where the agent runs. Default "vercel". */
|
|
7
|
+
host?: HostKind;
|
|
8
|
+
/** Department subagents. Default NONE. */
|
|
9
|
+
subagents?: string[];
|
|
10
|
+
/** Skip prompts and take the flags/defaults as given. */
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function init(rawName: string | undefined, options?: InitOptions): Promise<void>;
|
package/dist/init.js
CHANGED
|
@@ -1,86 +1,137 @@
|
|
|
1
1
|
import { cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, green, run, slug, yellow, } from "./util.js";
|
|
4
|
-
import { envExample, evalFileTs, evalScript, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
3
|
+
import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, yellow, } from "./util.js";
|
|
4
|
+
import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
5
5
|
import { suiteDir } from "./skills.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The always-installed core. Everything else — channels, subagents, engineer,
|
|
8
|
+
* host bindings — is opt-in, because assuming them means the FDE deletes files
|
|
9
|
+
* AND undoes real setup work (an Arcana workspace + scoped key per subagent).
|
|
10
|
+
*/
|
|
11
|
+
const CORE_ITEMS = ["enterprise", "arcana", "evals"];
|
|
12
|
+
// Official eve-registry limbs installed with the engineer subagent.
|
|
13
|
+
// connection/vercel is Vercel-Connect-backed, so it is VERCEL-HOST ONLY: on a
|
|
14
|
+
// self-hosted agent it cannot get an OIDC token and the agent fails to boot.
|
|
15
|
+
const ENGINEER_ITEMS_ALL = ["extension/agent-browser", "extension/github-tools"];
|
|
16
|
+
const ENGINEER_ITEMS_VERCEL = ["connection/vercel"];
|
|
17
|
+
const DEFAULT_MODEL = "anthropic/claude-sonnet-5";
|
|
18
|
+
export async function init(rawName, options = {}) {
|
|
19
|
+
const engineer = options.engineer === true;
|
|
20
|
+
const nonInteractive = options.yes === true;
|
|
11
21
|
const name = slug(rawName ?? (await ask("Agent name (kebab-case)?", "acme-agent")));
|
|
12
22
|
if (!name) {
|
|
13
23
|
console.error("An agent name is required.");
|
|
14
24
|
process.exit(1);
|
|
15
25
|
}
|
|
16
|
-
const displayName =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const displayName = nonInteractive
|
|
27
|
+
? name
|
|
28
|
+
: await ask("Display name (what employees call it)?", name);
|
|
29
|
+
// Channel: default NONE. A client on iMessage should never be handed Slack.
|
|
30
|
+
let channel = options.channel ?? "none";
|
|
31
|
+
if (!options.channel && !nonInteractive) {
|
|
32
|
+
const answer = await ask(`Chat surface? (${CHANNEL_KINDS.join(" | ")})`, "none");
|
|
33
|
+
const picked = answer.trim().toLowerCase();
|
|
34
|
+
channel = CHANNEL_KINDS.includes(picked) ? picked : "none";
|
|
35
|
+
}
|
|
36
|
+
// Host: where it runs. Vercel unless told otherwise.
|
|
37
|
+
let host = options.host ?? "vercel";
|
|
38
|
+
if (!options.host && !nonInteractive) {
|
|
39
|
+
const answer = await ask("Host? (vercel | exe)", "vercel");
|
|
40
|
+
host = answer.trim().toLowerCase() === "exe" ? "exe" : "vercel";
|
|
41
|
+
}
|
|
42
|
+
// Subagents: default NONE. Each one costs a workspace + scoped key.
|
|
43
|
+
let depts = options.subagents ?? [];
|
|
44
|
+
if (!options.subagents && !nonInteractive) {
|
|
45
|
+
const raw = await ask("Department subagents (comma-separated, empty for none)?", "");
|
|
46
|
+
depts = raw.split(",").map((d) => slug(d)).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
const issuer = nonInteractive
|
|
49
|
+
? DEFAULT_ISSUER
|
|
50
|
+
: await ask("Control-plane issuer?", DEFAULT_ISSUER);
|
|
23
51
|
closePrompts();
|
|
24
52
|
const dir = resolve(process.cwd(), name);
|
|
25
53
|
if (existsSync(dir)) {
|
|
26
54
|
console.error(`Directory ${name}/ already exists.`);
|
|
27
55
|
process.exit(1);
|
|
28
56
|
}
|
|
57
|
+
const plan = channelPlan(channel, name, host);
|
|
29
58
|
console.log(bold(`\n1/6 Scaffolding eve agent (eve@${EVE_VERSION}) …`));
|
|
30
59
|
run("npx", [`eve@${EVE_VERSION}`, "init", name]);
|
|
31
|
-
console.log(bold("\n2/6 Adding the Kybernesis registry + packages …"));
|
|
60
|
+
console.log(bold("\n2/6 Adding the Kybernesis registry + core packages …"));
|
|
32
61
|
run("npx", ["eve", "registry", "add", `@kybernesis=${REGISTRY_URL}`], { cwd: dir });
|
|
33
|
-
for (const item of
|
|
62
|
+
for (const item of CORE_ITEMS) {
|
|
34
63
|
run("npx", ["eve", "add", `@kybernesis/${item}`, "--overwrite"], { cwd: dir });
|
|
35
64
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
const extraDeps = [...plan.deps, ...(host === "exe" ? ["@kybernesis/exe", "@ai-sdk/openai"] : [])];
|
|
66
|
+
if (extraDeps.length) {
|
|
67
|
+
console.log(bold(`\n2b Installing for ${channel}/${host}: ${extraDeps.join(", ")} …`));
|
|
68
|
+
run("npm", ["install", ...extraDeps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
|
|
69
|
+
}
|
|
70
|
+
for (const item of plan.registryItems) {
|
|
71
|
+
run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
|
|
72
|
+
}
|
|
73
|
+
const engPlan = engineer ? engineerPlan(host, DEFAULT_MODEL) : null;
|
|
74
|
+
if (engPlan) {
|
|
75
|
+
console.log(bold("\n2c Engineer subagent: workshop sandbox + vision dev loop …"));
|
|
76
|
+
run("npm", ["install", ...engPlan.deps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
|
|
77
|
+
const engItems = [...ENGINEER_ITEMS_ALL, ...(host === "vercel" ? ENGINEER_ITEMS_VERCEL : [])];
|
|
78
|
+
for (const item of engItems) {
|
|
42
79
|
const ok = run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
|
|
43
80
|
if (!ok)
|
|
44
81
|
console.log(yellow(` ! ${item} did not install cleanly — re-run: npx eve add ${item}`));
|
|
45
82
|
}
|
|
46
83
|
}
|
|
47
|
-
console.log(bold("\
|
|
84
|
+
console.log(bold("\n2d Seeding the FDE Claude Code skill suite (.claude/skills) …"));
|
|
48
85
|
try {
|
|
49
86
|
cpSync(suiteDir(), join(dir, ".claude/skills"), { recursive: true });
|
|
50
87
|
}
|
|
51
88
|
catch {
|
|
52
89
|
console.log(yellow(" ! skill suite not found — run kyb skills inside the repo later"));
|
|
53
90
|
}
|
|
54
|
-
console.log(bold("\n3/6 Writing
|
|
91
|
+
console.log(bold("\n3/6 Writing identity, model config, memory mount, and evals …"));
|
|
55
92
|
mkdirSync(join(dir, "agent/instructions"), { recursive: true });
|
|
56
93
|
writeFileSync(join(dir, "agent/instructions/identity.md"), identityMd(displayName, depts));
|
|
57
|
-
// The scaffold ships a flat agent/instructions.md; the directory form wins,
|
|
58
|
-
// so remove the flat file to avoid ambiguity.
|
|
59
94
|
try {
|
|
60
95
|
unlinkSync(join(dir, "agent/instructions.md"));
|
|
61
96
|
}
|
|
62
97
|
catch { }
|
|
98
|
+
writeFileSync(join(dir, "agent/agent.ts"), hostAgentTs(host, DEFAULT_MODEL));
|
|
63
99
|
writeFileSync(join(dir, "agent/extensions/arcana.ts"), rootArcanaTs());
|
|
64
100
|
writeFileSync(join(dir, "evals/kybernesis.eval.ts"), evalFileTs(displayName, depts));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
if (plan.file) {
|
|
102
|
+
console.log(bold(`\n4/6 Channel: ${channel} …`));
|
|
103
|
+
mkdirSync(join(dir, "agent/channels"), { recursive: true });
|
|
104
|
+
writeFileSync(join(dir, "agent/channels", plan.file), plan.content);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(bold(`\n4/6 Channel: ${channel} (no channel file) …`));
|
|
108
|
+
}
|
|
109
|
+
if (depts.length) {
|
|
110
|
+
console.log(bold(`\n4b Generating ${depts.length} department subagent(s) …`));
|
|
111
|
+
const skillsSource = join(dir, "node_modules/@kybernesis/arcana/dist/extension/skills");
|
|
112
|
+
for (const dept of depts) {
|
|
113
|
+
const base = join(dir, "agent/subagents", dept);
|
|
114
|
+
mkdirSync(join(base, "connections"), { recursive: true });
|
|
115
|
+
writeFileSync(join(base, "agent.ts"), subagentAgentTs(dept));
|
|
116
|
+
writeFileSync(join(base, "instructions.md"), subagentInstructionsMd(dept));
|
|
117
|
+
writeFileSync(join(base, "connections/arcana.ts"), subagentArcanaTs(dept));
|
|
118
|
+
if (existsSync(skillsSource)) {
|
|
119
|
+
cpSync(skillsSource, join(base, "skills"), { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(yellow(` ! arcana skills not found at ${skillsSource} — copy them manually`));
|
|
123
|
+
}
|
|
77
124
|
}
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
}
|
|
126
|
+
if (engPlan) {
|
|
127
|
+
for (const file of engPlan.files) {
|
|
128
|
+
const full = join(dir, file.path);
|
|
129
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
130
|
+
writeFileSync(full, file.content);
|
|
80
131
|
}
|
|
81
132
|
}
|
|
82
133
|
console.log(bold("\n5/6 Env template + hermetic eval script …"));
|
|
83
|
-
writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer));
|
|
134
|
+
writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer, plan.env));
|
|
84
135
|
const pkgPath = join(dir, "package.json");
|
|
85
136
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
86
137
|
pkg.scripts = { ...pkg.scripts, eval: evalScript(name, depts) };
|
|
@@ -88,34 +139,33 @@ export async function init(rawName, options) {
|
|
|
88
139
|
console.log(bold("\n6/6 Verifying (typecheck + eve discovery) …"));
|
|
89
140
|
const tsOk = run("npm", ["run", "typecheck"], { cwd: dir, allowFail: true });
|
|
90
141
|
const infoOk = run("npx", ["eve", "info"], { cwd: dir, allowFail: true, quiet: true });
|
|
91
|
-
console.log(tsOk && infoOk
|
|
142
|
+
console.log(tsOk && infoOk
|
|
143
|
+
? green(" ✓ typecheck + discovery clean")
|
|
144
|
+
: yellow(" ! verify manually: npm run typecheck && npx eve info"));
|
|
145
|
+
const parts = [
|
|
146
|
+
"governed (enterprise)",
|
|
147
|
+
"remembering (arcana)",
|
|
148
|
+
"self-testing (evals)",
|
|
149
|
+
channel === "none" ? null : `${channel} channel`,
|
|
150
|
+
host === "exe" ? "exe.dev host" : null,
|
|
151
|
+
engineer ? "engineer subagent (workshop + vision loop)" : null,
|
|
152
|
+
depts.length ? `${depts.length} dept subagent(s)` : null,
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
const steps = [
|
|
155
|
+
`Arcana: create workspaces (${name}-company, ${name}-eval${depts.map((d) => `, ${name}-${d}`).join("")}) + scoped kb_ keys; fill .env.local from .env.example`,
|
|
156
|
+
...hostSteps(host, name),
|
|
157
|
+
...plan.steps,
|
|
158
|
+
...(engPlan?.steps ?? []),
|
|
159
|
+
`Control plane: register agent "${name}" at ${issuer}/agents + grant the pilot cohort`,
|
|
160
|
+
`npm run eval → green → deploy → live smoke + the revoke demo`,
|
|
161
|
+
];
|
|
92
162
|
console.log(`
|
|
93
|
-
${green("✓")} ${bold(name)} scaffolded:
|
|
94
|
-
|
|
95
|
-
${bold("Engineer notes:")}
|
|
96
|
-
· The workshop sandbox (agent/sandbox/sandbox.ts) bakes Playwright into the
|
|
97
|
-
template at DEPLOY time — a broken bootstrap fails the Vercel build loudly.
|
|
98
|
-
Deployed sessions run under a domain allowlist; extend it deliberately in
|
|
99
|
-
that file when a project needs another host.
|
|
100
|
-
· Vercel connection (preview deploys + link-back), after \`vercel link\`:
|
|
101
|
-
vercel connect create mcp.vercel.com --name vercel
|
|
102
|
-
vercel connect attach mcp.vercel.com/vercel --yes
|
|
103
|
-
then set connect("mcp.vercel.com/vercel") — the UID, not the short name —
|
|
104
|
-
in agent/connections/vercel.ts. First tool use posts an OAuth link in the
|
|
105
|
-
thread (user-scoped); grant "All projects" so the agent can create new ones,
|
|
106
|
-
then narrow the grant in the dashboard once the project exists.
|
|
107
|
-
· File delivery (the deliver tool needs it):
|
|
108
|
-
vercel blob create-store ${name}-deliverables --access public --yes
|
|
109
|
-
links the store and injects BLOB_READ_WRITE_TOKEN automatically.
|
|
110
|
-
· agent-browser / github-tools may need their Connect setup flows — run
|
|
111
|
-
their printed setup commands if tools 401.` : ""}
|
|
163
|
+
${green("✓")} ${bold(name)} scaffolded: ${parts.join(" · ")}
|
|
112
164
|
|
|
113
165
|
${bold("Human steps (in order) — the FDE playbook covers each in detail:")}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
3. Slack: \`vercel connect create slack --triggers --name ${name}\` → detach → re-attach with --trigger-path /eve/v1/slack
|
|
117
|
-
4. Control plane: register agent "${name}" (runtime: ▲ eve) at ${issuer}/agents + grant the pilot cohort
|
|
118
|
-
5. \`npm run eval\` → green → \`npx eve deploy\` → live Slack smoke + the revoke demo
|
|
166
|
+
${steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
|
|
167
|
+
|
|
119
168
|
Run ${bold("kyb doctor")} inside ${name}/ any time to check the wiring.
|
|
169
|
+
${dim(" Add more later: kyb add channel <kind> · kyb add subagent <name>")}
|
|
120
170
|
`);
|
|
121
171
|
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -7,5 +7,33 @@ export declare function subagentInstructionsMd(dept: string): string;
|
|
|
7
7
|
export declare function subagentArcanaTs(dept: string): string;
|
|
8
8
|
export declare function rootArcanaTs(): string;
|
|
9
9
|
export declare function evalFileTs(displayName: string, depts: string[]): string;
|
|
10
|
-
export declare function envExample(name: string, depts: string[], issuer: string): string;
|
|
10
|
+
export declare function envExample(name: string, depts: string[], issuer: string, channelEnv?: string[]): string;
|
|
11
11
|
export declare function evalScript(name: string, depts: string[]): string;
|
|
12
|
+
export type ChannelKind = "none" | "slack" | "imessage" | "telegram" | "discord" | "web";
|
|
13
|
+
export declare const CHANNEL_KINDS: ChannelKind[];
|
|
14
|
+
export interface ChannelPlan {
|
|
15
|
+
/** File written under agent/channels/, or null for `none`. */
|
|
16
|
+
file: string | null;
|
|
17
|
+
content: string;
|
|
18
|
+
/** npm deps this channel needs beyond eve. */
|
|
19
|
+
deps: string[];
|
|
20
|
+
/** Registry items to `eve add` (interactive setup flows live there). */
|
|
21
|
+
registryItems: string[];
|
|
22
|
+
/** Env lines appended to .env.example. */
|
|
23
|
+
env: string[];
|
|
24
|
+
/** Human setup steps printed after scaffolding. */
|
|
25
|
+
steps: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function channelPlan(kind: ChannelKind, name: string, host: HostKind): ChannelPlan;
|
|
28
|
+
export type HostKind = "vercel" | "exe";
|
|
29
|
+
export declare function hostAgentTs(host: HostKind, model: string): string;
|
|
30
|
+
export declare function hostSteps(host: HostKind, name: string): string[];
|
|
31
|
+
export interface EngineerPlan {
|
|
32
|
+
files: Array<{
|
|
33
|
+
path: string;
|
|
34
|
+
content: string;
|
|
35
|
+
}>;
|
|
36
|
+
deps: string[];
|
|
37
|
+
steps: string[];
|
|
38
|
+
}
|
|
39
|
+
export declare function engineerPlan(host: HostKind, model: string): EngineerPlan;
|
package/dist/templates.js
CHANGED
|
@@ -120,7 +120,7 @@ export default kybernesisBaseline({
|
|
|
120
120
|
${depts.length ? ` routing: [\n${routing}\n ],\n` : ""}});
|
|
121
121
|
`;
|
|
122
122
|
}
|
|
123
|
-
export function envExample(name, depts, issuer) {
|
|
123
|
+
export function envExample(name, depts, issuer, channelEnv = []) {
|
|
124
124
|
const deptVars = depts
|
|
125
125
|
.map((d) => {
|
|
126
126
|
const upper = d.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
@@ -135,10 +135,7 @@ export function envExample(name, depts, issuer) {
|
|
|
135
135
|
KYBERNESIS_ISSUER="${issuer}"
|
|
136
136
|
KYBERNESIS_AGENT="${name}"
|
|
137
137
|
|
|
138
|
-
#
|
|
139
|
-
# SLACK_CONNECTOR_UID="slack/${name}"
|
|
140
|
-
|
|
141
|
-
# Arcana memory (@kybernesis/arcana) — one workspace + scoped kb_ key per brain
|
|
138
|
+
${channelEnv.length ? `# Channel\n${channelEnv.join("\n")}\n\n` : ""}# Arcana memory (@kybernesis/arcana) — one workspace + scoped kb_ key per brain
|
|
142
139
|
# ARCANA_API_KEY="kb_..."
|
|
143
140
|
# ARCANA_COMPANY_WORKSPACE="${name}-company"
|
|
144
141
|
# ARCANA_DM_WORKSPACE="${name}-company"
|
|
@@ -154,3 +151,359 @@ export function evalScript(name, depts) {
|
|
|
154
151
|
];
|
|
155
152
|
return `${overrides.join(" ")} eve eval`;
|
|
156
153
|
}
|
|
154
|
+
export const CHANNEL_KINDS = [
|
|
155
|
+
"none",
|
|
156
|
+
"slack",
|
|
157
|
+
"imessage",
|
|
158
|
+
"telegram",
|
|
159
|
+
"discord",
|
|
160
|
+
"web",
|
|
161
|
+
];
|
|
162
|
+
export function channelPlan(kind, name, host) {
|
|
163
|
+
const onExe = host === "exe";
|
|
164
|
+
switch (kind) {
|
|
165
|
+
case "slack":
|
|
166
|
+
return {
|
|
167
|
+
file: "slack.ts",
|
|
168
|
+
content: onExe
|
|
169
|
+
? `import { multiplayerSlackChannel } from "@kybernesis/multiplayer/slack";
|
|
170
|
+
import { forwardedSocketVerifier } from "@kybernesis/exe/slack";
|
|
171
|
+
|
|
172
|
+
// Slack on a self-hosted (exe.dev) host.
|
|
173
|
+
// Inbound: a forwarder holds the exe-brokered Socket Mode connection and POSTs
|
|
174
|
+
// events here; the verifier authenticates them on SLACK_SOCKET_FORWARDING_SECRET.
|
|
175
|
+
// Outbound: SLACK_BOT_TOKEN must be on the host — eve's SlackChannelCredentials
|
|
176
|
+
// has no apiUrl, so calls can't route through the exe integration yet.
|
|
177
|
+
export default multiplayerSlackChannel({
|
|
178
|
+
credentials: {
|
|
179
|
+
botToken: process.env.SLACK_BOT_TOKEN!,
|
|
180
|
+
webhookVerifier: forwardedSocketVerifier(),
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
`
|
|
184
|
+
: `import { connectSlackCredentials } from "@vercel/connect/eve";
|
|
185
|
+
import { multiplayerSlackChannel } from "@kybernesis/multiplayer/slack";
|
|
186
|
+
|
|
187
|
+
// Shared threads with per-speaker verified identity, attributed context, and
|
|
188
|
+
// dual surface (channel vs DM). Credentials are brokered by Vercel Connect.
|
|
189
|
+
export default multiplayerSlackChannel({
|
|
190
|
+
credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR_UID!),
|
|
191
|
+
});
|
|
192
|
+
`,
|
|
193
|
+
deps: ["@kybernesis/multiplayer", ...(onExe ? ["@kybernesis/exe"] : ["@vercel/connect"])],
|
|
194
|
+
registryItems: [],
|
|
195
|
+
env: onExe
|
|
196
|
+
? [
|
|
197
|
+
`SLACK_BOT_TOKEN="xoxb-..." # outbound; from Slack OAuth & Permissions`,
|
|
198
|
+
`SLACK_SOCKET_FORWARDING_SECRET="$(openssl rand -hex 24)"`,
|
|
199
|
+
]
|
|
200
|
+
: [`SLACK_CONNECTOR_UID="slack/${name}"`],
|
|
201
|
+
steps: onExe
|
|
202
|
+
? [
|
|
203
|
+
`Create a Slack app (Socket Mode on; scopes: app_mentions:read, chat:write, channels:history, groups:history, im:history, users:read)`,
|
|
204
|
+
`Hold its tokens off-host: ssh exe.dev integrations add slack --name ${name} --bot-token=- --app-token=-`,
|
|
205
|
+
`Run the forwarder (scripts/slack-forwarder.py in @kybernesis/exe) with EXE_SLACK_GW set`,
|
|
206
|
+
`After ANY scope change, reinstall the app — a stale token silently stops receiving events`,
|
|
207
|
+
]
|
|
208
|
+
: [
|
|
209
|
+
`vercel connect create slack --triggers --name ${name}`,
|
|
210
|
+
`vercel connect detach <uid> --yes && vercel connect attach <uid> --triggers --trigger-path /eve/v1/slack --yes`,
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
case "imessage":
|
|
214
|
+
return {
|
|
215
|
+
file: "photon.ts",
|
|
216
|
+
content: `import { photonIMessageChannel } from "eve/channels/photon";
|
|
217
|
+
${onExe ? `import { photonEnvCredentials } from "@kybernesis/exe/photon";\n` : ""}
|
|
218
|
+
// iMessage via Photon. Inbound is a plain webhook at /eve/v1/photon; the Photon
|
|
219
|
+
// signing secret takes precedence over the default Vercel-OIDC verifier, so this
|
|
220
|
+
// works on any host with a public HTTPS URL.
|
|
221
|
+
export default photonIMessageChannel({
|
|
222
|
+
${onExe
|
|
223
|
+
? ` credentials: photonEnvCredentials(),`
|
|
224
|
+
: ` async credentials() {
|
|
225
|
+
const projectId = process.env.IMESSAGE_PROJECT_ID;
|
|
226
|
+
const projectSecret = process.env.IMESSAGE_PROJECT_SECRET;
|
|
227
|
+
if (!projectId || !projectSecret) throw new Error("Photon project credentials are required.");
|
|
228
|
+
return { projectId, projectSecret };
|
|
229
|
+
},`}
|
|
230
|
+
webhookSecret: process.env.IMESSAGE_WEBHOOK_SECRET,
|
|
231
|
+
});
|
|
232
|
+
`,
|
|
233
|
+
deps: onExe ? ["@kybernesis/exe"] : [],
|
|
234
|
+
registryItems: [],
|
|
235
|
+
env: [
|
|
236
|
+
`IMESSAGE_PROJECT_ID="..."`,
|
|
237
|
+
`IMESSAGE_PROJECT_SECRET="..."`,
|
|
238
|
+
`IMESSAGE_WEBHOOK_SECRET="..." # Photon webhook signing secret`,
|
|
239
|
+
],
|
|
240
|
+
steps: [
|
|
241
|
+
`Create a Photon project and register the phone number (npx eve add channel/photon-imessage walks it)`,
|
|
242
|
+
onExe
|
|
243
|
+
? `Make the host public FIRST — webhooks need anonymous access:\n ssh exe.dev share port <vm> 8000 && ssh exe.dev share set-public <vm>`
|
|
244
|
+
: `Deploy so the public URL exists`,
|
|
245
|
+
`Register a Photon webhook for https://<your-host>/eve/v1/photon and copy its signing secret to IMESSAGE_WEBHOOK_SECRET`,
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
case "telegram":
|
|
249
|
+
return {
|
|
250
|
+
file: "telegram.ts",
|
|
251
|
+
content: `import { telegramChannel } from "eve/channels/telegram";
|
|
252
|
+
|
|
253
|
+
// Telegram. Register the webhook against your public URL after deploying.
|
|
254
|
+
export default telegramChannel({ botToken: process.env.TELEGRAM_BOT_TOKEN! });
|
|
255
|
+
`,
|
|
256
|
+
deps: [],
|
|
257
|
+
registryItems: [],
|
|
258
|
+
env: [`TELEGRAM_BOT_TOKEN="..." # from @BotFather`],
|
|
259
|
+
steps: [
|
|
260
|
+
`Create a bot with @BotFather and copy its token`,
|
|
261
|
+
`After deploy: curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" -d "url=https://<host>/eve/v1/telegram"`,
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
case "discord":
|
|
265
|
+
return {
|
|
266
|
+
file: "discord.ts",
|
|
267
|
+
content: `import { discordChannel } from "eve/channels/discord";
|
|
268
|
+
|
|
269
|
+
export default discordChannel({ botToken: process.env.DISCORD_BOT_TOKEN! });
|
|
270
|
+
`,
|
|
271
|
+
deps: [],
|
|
272
|
+
registryItems: [],
|
|
273
|
+
env: [`DISCORD_BOT_TOKEN="..."`],
|
|
274
|
+
steps: [`Create a Discord application + bot, invite it, set DISCORD_BOT_TOKEN`],
|
|
275
|
+
};
|
|
276
|
+
case "web":
|
|
277
|
+
return {
|
|
278
|
+
file: null,
|
|
279
|
+
content: "",
|
|
280
|
+
deps: [],
|
|
281
|
+
registryItems: [],
|
|
282
|
+
env: [],
|
|
283
|
+
steps: [
|
|
284
|
+
`The eve channel (agent/channels/eve.ts) already serves HTTP; build a frontend with useEveAgent`,
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
case "none":
|
|
288
|
+
default:
|
|
289
|
+
return {
|
|
290
|
+
file: null,
|
|
291
|
+
content: "",
|
|
292
|
+
deps: [],
|
|
293
|
+
registryItems: [],
|
|
294
|
+
env: [],
|
|
295
|
+
steps: [`No chat surface yet — add one later with: kyb add channel <slack|imessage|telegram|discord|web>`],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export function hostAgentTs(host, model) {
|
|
300
|
+
if (host === "exe") {
|
|
301
|
+
return `import { defineAgent } from "eve";
|
|
302
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
303
|
+
import { exeModel } from "@kybernesis/exe";
|
|
304
|
+
|
|
305
|
+
// Model served by the exe.dev LLM integration — no provider key on the host.
|
|
306
|
+
// exe injects the credential (managed gateway, your API key, or a connected
|
|
307
|
+
// ChatGPT subscription) server-side.
|
|
308
|
+
export default defineAgent({
|
|
309
|
+
model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(model)}, createOpenAI }),
|
|
310
|
+
modelContextWindowTokens: 200_000,
|
|
311
|
+
});
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
return `import { defineAgent } from "eve";
|
|
315
|
+
|
|
316
|
+
export default defineAgent({
|
|
317
|
+
model: ${JSON.stringify(model)},
|
|
318
|
+
});
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
export function hostSteps(host, name) {
|
|
322
|
+
if (host === "exe") {
|
|
323
|
+
return [
|
|
324
|
+
`Create the VM: ssh exe.dev new --name ${name}`,
|
|
325
|
+
`Attach an LLM integration (ChatGPT subscription, your API key, or exe's gateway):`,
|
|
326
|
+
` ssh exe.dev integrations setup chatgpt --name work # once, device-code`,
|
|
327
|
+
` ssh exe.dev integrations edit llm --openai=chatgpt --openai-account=work`,
|
|
328
|
+
`Install Node 24 + deps on the VM, then: npx eve build && bash scripts/eve-server.sh start`,
|
|
329
|
+
`\`eve start\` does NOT read .env.local — scripts/eve-server.sh loads it for you`,
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
return [
|
|
333
|
+
`vercel link (the client's team), then set envs (prod/preview Sensitive)`,
|
|
334
|
+
`npx eve deploy`,
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
/** The workshop sandbox, per host. Same recipe; different backend. */
|
|
338
|
+
function workshopSandbox(host) {
|
|
339
|
+
if (host === "exe") {
|
|
340
|
+
return `import { defineSandbox } from "eve/sandbox";
|
|
341
|
+
import { docker } from "eve/sandbox/docker";
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The engineer workshop, self-hosted: pnpm + Playwright + Chromium baked into
|
|
345
|
+
* the TEMPLATE so warm sessions run render→screenshot→vision in seconds.
|
|
346
|
+
*
|
|
347
|
+
* Docker rather than vercel(): the hosted backend needs Vercel OIDC, which does
|
|
348
|
+
* not exist off-Vercel.
|
|
349
|
+
*
|
|
350
|
+
* HOST PREREQUISITE: some images ship Docker disabled (exe.dev's exeuntu runs
|
|
351
|
+
* \`systemctl disable docker.service\`). Run \`sudo systemctl enable --now docker\`
|
|
352
|
+
* or every build fails with SandboxTemplateNotProvisionedError.
|
|
353
|
+
*
|
|
354
|
+
* NOTE: Docker sessions do not enforce a domain allowlist the way the hosted
|
|
355
|
+
* backend does. Egress control is the HOST's responsibility here — a deliberate
|
|
356
|
+
* difference from the Vercel deployment, not an oversight.
|
|
357
|
+
*/
|
|
358
|
+
export default defineSandbox({
|
|
359
|
+
backend: docker(),
|
|
360
|
+
revalidationKey: () => "kybernesis-workshop-v5-docker",
|
|
361
|
+
async bootstrap({ use }) {
|
|
362
|
+
const sandbox = await use();
|
|
363
|
+
await sandbox.run({ command: "apt-get update" });
|
|
364
|
+
await sandbox.run({ command: "npm install -g pnpm" });
|
|
365
|
+
await sandbox.run({
|
|
366
|
+
command:
|
|
367
|
+
"mkdir -p /workspace/.shot && cd /workspace/.shot && echo '{\\"name\\":\\"kyb-shot\\",\\"private\\":true}' > package.json && npm install playwright",
|
|
368
|
+
});
|
|
369
|
+
await sandbox.run({
|
|
370
|
+
command: "cd /workspace/.shot && npx playwright install --with-deps chromium",
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
return `import { defineSandbox } from "eve/sandbox";
|
|
377
|
+
import { vercel } from "eve/sandbox/vercel";
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The engineer workshop: a warm, safe cloud dev machine.
|
|
381
|
+
*
|
|
382
|
+
* TEMPLATE bootstrap (once, inherited by every session): pnpm + Playwright +
|
|
383
|
+
* Chromium. Prewarm runs at deploy time, so a broken bootstrap fails the build
|
|
384
|
+
* loudly and warm sessions run the full render→screenshot→vision loop in
|
|
385
|
+
* seconds. Backend PINNED to Vercel Sandbox — hosted sandboxes even from local
|
|
386
|
+
* dev (run \`vercel link\` + \`vercel env pull\` first), so evals exercise the
|
|
387
|
+
* exact production backend. No Docker anywhere.
|
|
388
|
+
*
|
|
389
|
+
* All sessions run under a domain ALLOWLIST: an agent that installs arbitrary
|
|
390
|
+
* npm packages must not have open egress. A blocked domain fails loudly;
|
|
391
|
+
* treat every addition as a security decision.
|
|
392
|
+
*/
|
|
393
|
+
export default defineSandbox({
|
|
394
|
+
backend: vercel({
|
|
395
|
+
resources: { vcpus: 4 },
|
|
396
|
+
networkPolicy: {
|
|
397
|
+
allow: [
|
|
398
|
+
"registry.npmjs.org",
|
|
399
|
+
"*.npmjs.org",
|
|
400
|
+
"github.com",
|
|
401
|
+
"api.github.com",
|
|
402
|
+
"codeload.github.com",
|
|
403
|
+
"*.githubusercontent.com",
|
|
404
|
+
"cdn.playwright.dev",
|
|
405
|
+
"playwright.azureedge.net",
|
|
406
|
+
"playwright.download.prss.microsoft.com",
|
|
407
|
+
"storage.googleapis.com",
|
|
408
|
+
"archive.ubuntu.com",
|
|
409
|
+
"security.ubuntu.com",
|
|
410
|
+
"ports.ubuntu.com",
|
|
411
|
+
"*.ubuntu.com",
|
|
412
|
+
"deb.debian.org",
|
|
413
|
+
"security.debian.org",
|
|
414
|
+
"*.debian.org",
|
|
415
|
+
"ai-gateway.vercel.sh",
|
|
416
|
+
"vercel.com",
|
|
417
|
+
"*.vercel.app",
|
|
418
|
+
"fonts.googleapis.com",
|
|
419
|
+
"fonts.gstatic.com",
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
revalidationKey: () => "kybernesis-workshop-v5",
|
|
424
|
+
async bootstrap({ use }) {
|
|
425
|
+
const sandbox = await use();
|
|
426
|
+
// The egress proxy carries HTTPS only; apt defaults to http:// mirrors, so
|
|
427
|
+
// every index fetch silently fails. Rewrite to https first.
|
|
428
|
+
await sandbox.run({
|
|
429
|
+
command:
|
|
430
|
+
"find /etc/apt -type f \\\\( -name '*.list' -o -name '*.sources' \\\\) -exec sed -i 's|http://|https://|g' {} + && apt-get update",
|
|
431
|
+
});
|
|
432
|
+
await sandbox.run({ command: "npm install -g pnpm" });
|
|
433
|
+
await sandbox.run({
|
|
434
|
+
command:
|
|
435
|
+
"mkdir -p /workspace/.shot && cd /workspace/.shot && echo '{\\"name\\":\\"kyb-shot\\",\\"private\\":true}' > package.json && npm install playwright",
|
|
436
|
+
});
|
|
437
|
+
await sandbox.run({
|
|
438
|
+
command: "cd /workspace/.shot && npx playwright install --with-deps chromium",
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
444
|
+
export function engineerPlan(host, model) {
|
|
445
|
+
const onExe = host === "exe";
|
|
446
|
+
const files = [
|
|
447
|
+
{
|
|
448
|
+
path: "agent/subagents/builder/agent.ts",
|
|
449
|
+
content: onExe
|
|
450
|
+
? `import { defineAgent } from "eve";
|
|
451
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
452
|
+
import { exeModel } from "@kybernesis/exe";
|
|
453
|
+
|
|
454
|
+
// The specialist the root agent delegates BUILDING to. \`description\` is what
|
|
455
|
+
// the root routes on — keep it about building, not answering.
|
|
456
|
+
export default defineAgent({
|
|
457
|
+
description:
|
|
458
|
+
"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.",
|
|
459
|
+
model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(model)}, createOpenAI }),
|
|
460
|
+
modelContextWindowTokens: 200_000,
|
|
461
|
+
});
|
|
462
|
+
`
|
|
463
|
+
: `import { defineAgent } from "eve";
|
|
464
|
+
|
|
465
|
+
// The specialist the root agent delegates BUILDING to. \`description\` is what
|
|
466
|
+
// the root routes on — keep it about building, not answering.
|
|
467
|
+
export default defineAgent({
|
|
468
|
+
description:
|
|
469
|
+
"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.",
|
|
470
|
+
model: ${JSON.stringify(model)},
|
|
471
|
+
});
|
|
472
|
+
`,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
path: "agent/subagents/builder/extensions/engineer.ts",
|
|
476
|
+
content: `// Engineer layer mounted LOCALLY on this subagent (eve >=0.30): screenshot,
|
|
477
|
+
// deliver, and the trade-school skills belong to \`builder\` alone. The root
|
|
478
|
+
// agent never gets shell or a browser.
|
|
479
|
+
export { default } from "@kybernesis/engineer";
|
|
480
|
+
`,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
path: "agent/subagents/builder/sandbox/sandbox.ts",
|
|
484
|
+
content: workshopSandbox(host),
|
|
485
|
+
},
|
|
486
|
+
];
|
|
487
|
+
if (onExe) {
|
|
488
|
+
files.push({
|
|
489
|
+
path: "agent/subagents/builder/tools/preview.ts",
|
|
490
|
+
content: `export { previewTool as default } from "@kybernesis/exe/preview";
|
|
491
|
+
`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
files,
|
|
496
|
+
deps: onExe ? ["@kybernesis/engineer", "@kybernesis/exe"] : ["@kybernesis/engineer"],
|
|
497
|
+
steps: onExe
|
|
498
|
+
? [
|
|
499
|
+
"Enable Docker on the host (some images ship it disabled): sudo systemctl enable --now docker",
|
|
500
|
+
"Preview server (so the agent can show you what it built):\n mkdir -p ~/preview && setsid python3 -m http.server 3456 --directory ~/preview &\n then open https://<vm>.exe.xyz:3456/<file> (account-gated, not public)",
|
|
501
|
+
"File delivery needs object storage: set BLOB_READ_WRITE_TOKEN (Vercel Blob) or DELIVER_DIR + DELIVER_BASE_URL to serve from this host",
|
|
502
|
+
"Public deploys need the client's own Vercel token — Vercel Connect does NOT work off-Vercel",
|
|
503
|
+
]
|
|
504
|
+
: [
|
|
505
|
+
"File delivery: vercel blob create-store <name>-deliverables --access public --yes",
|
|
506
|
+
"Preview deploys: eve add connection/vercel, then vercel connect create mcp.vercel.com --name vercel && vercel connect attach mcp.vercel.com/vercel --yes",
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -904,6 +904,21 @@ Client-conversation rules of thumb:
|
|
|
904
904
|
product conversation — purpose-scoped grants, §2.5 disclosures. Don't wire
|
|
905
905
|
one as if it were internal.
|
|
906
906
|
|
|
907
|
+
**Governed mode (dispatch ≥0.2.1 + enterprise ≥0.2.0 + the client's control
|
|
908
|
+
plane) — the preferred form.** Edges become GRANTS in the admin instead of
|
|
909
|
+
code: register both agents (/agents, OPEN production alias, health 200), grant
|
|
910
|
+
the edge on the CALLEE's panel (caller + purpose + optional expiry), mint each
|
|
911
|
+
agent's credential (shown once) into KYBERNESIS_AGENT_CREDENTIAL on its
|
|
912
|
+
deployment. Code shrinks to remotePeer({ callee: "<EXACT registered name —
|
|
913
|
+
case-sensitive>", governed: { issuer }, envVar, fallbackUrl }) and
|
|
914
|
+
dispatchChannel({ governed: { issuer, agent } }). Outbound auth is a 300 s A2A
|
|
915
|
+
token minted per edge; the callee URL comes from the registry (discovery), env
|
|
916
|
+
var still wins. THE DEMO: revoke the edge in the admin → the caller is refused
|
|
917
|
+
(edge_not_granted) within 5 minutes, no redeploy; re-grant → restored. Run it
|
|
918
|
+
for the client — it's the whole governance story in one minute. Full lifecycle
|
|
919
|
+
proven live 2026-08-07 (kyber ↔ eve-gtm). Budget note: the deployed agent and
|
|
920
|
+
local eval runs share the project's AI Gateway budget — size it for both.
|
|
921
|
+
|
|
907
922
|
### 4.4 Author the agent's identity and instructions
|
|
908
923
|
|
|
909
924
|
How instructions work in eve (30 seconds of mechanics): a flat
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use when deploying an eve agent OFF Vercel — on exe.dev, a VPS, or any client infrastructure — or when a client wants to use their own ChatGPT/LLM subscription. Covers what breaks, what replaces it, and the credential checklist.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Self-hosted agents (client infrastructure, not Vercel)
|
|
6
|
+
|
|
7
|
+
The Vercel path is the default and the proven one. Reach for this when the
|
|
8
|
+
client **won't or can't use Vercel**, or wants their agent's inference billed to
|
|
9
|
+
a subscription they already pay for.
|
|
10
|
+
|
|
11
|
+
**The governing rule: everything must come from the CLIENT's accounts.** If a
|
|
12
|
+
step only works because you happen to hold a credential, that step is a bug in
|
|
13
|
+
the deployment, not a shortcut. It will fail on the real engagement.
|
|
14
|
+
|
|
15
|
+
## Scaffold
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer
|
|
19
|
+
```
|
|
20
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## What Vercel gives you that a client host does not
|
|
25
|
+
|
|
26
|
+
| Capability | On Vercel | Self-hosted replacement |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Model access | AI Gateway | exe.dev LLM integration (`exeModel`) — managed, BYO key, or a **ChatGPT subscription** |
|
|
29
|
+
| Slack/Photon/Linear credentials | Vercel Connect | **Portable/static credentials the client issues** |
|
|
30
|
+
| Sandbox | `vercel()` hosted | `docker()` on the host |
|
|
31
|
+
| File delivery | Vercel Blob | Blob **or** `DELIVER_DIR` + `DELIVER_BASE_URL` |
|
|
32
|
+
| Public URLs | deployments | a deploy target, or an account-gated preview |
|
|
33
|
+
| Secrets | Vercel env | host env + the platform's own secret injection |
|
|
34
|
+
|
|
35
|
+
**Vercel Connect does not work off-Vercel — at all.** It authenticates via
|
|
36
|
+
Vercel OIDC, which does not exist on another host. That applies to Slack, the
|
|
37
|
+
Vercel MCP connection, Linear, everything. Each becomes a static credential
|
|
38
|
+
someone must issue and rotate. `kyb doctor` fails loudly if a `@vercel/connect`
|
|
39
|
+
import survives into a self-hosted agent.
|
|
40
|
+
|
|
41
|
+
## The failure modes, each of which cost a real session
|
|
42
|
+
|
|
43
|
+
- **Docker ships disabled on some images.** exe.dev's exeuntu runs
|
|
44
|
+
`systemctl disable docker.service`, so `docker --version` works while nothing
|
|
45
|
+
can run. Every sandbox call fails with `SandboxTemplateNotProvisionedError`.
|
|
46
|
+
Fix: `sudo systemctl enable --now docker`.
|
|
47
|
+
- **Subagents own their sandbox — they do NOT inherit the root's.** An engineer
|
|
48
|
+
subagent without its own `sandbox/sandbox.ts` gets a bare template, and the
|
|
49
|
+
screenshot tool fails with `Cannot find module 'playwright'` while the root's
|
|
50
|
+
template is fine.
|
|
51
|
+
- **`eve start` does not read `.env.local`** the way `eve dev` does. Export it
|
|
52
|
+
into the process (`scripts/eve-server.sh` in `@kybernesis/exe` does this).
|
|
53
|
+
- **Prewarm lives in the eve CLI, not the built server.** Starting
|
|
54
|
+
`node .output/server/index.mjs` directly gives you clean logs but skips
|
|
55
|
+
template prewarm entirely. Start with `npx eve start`.
|
|
56
|
+
- **`localDev()` never authenticates under `eve start`** — it is a property of
|
|
57
|
+
the deployment, not the request. A self-hosted agent needs a real
|
|
58
|
+
authenticator from day one.
|
|
59
|
+
- **`pkill -f <pattern>` over SSH kills your own session** when the pattern
|
|
60
|
+
appears in the SSH command line — and can take the agent with it. Use a
|
|
61
|
+
pidfile (`scripts/eve-server.sh`).
|
|
62
|
+
- **Never diagnose "nothing is happening" from a log file.** Count runs on disk:
|
|
63
|
+
`.eve/.workflow-data/runs/`. A log can look frozen at boot while the agent
|
|
64
|
+
serves happily.
|
|
65
|
+
|
|
66
|
+
## Showing the client what the agent built
|
|
67
|
+
|
|
68
|
+
- **Vercel Blob refuses to serve HTML inline** — it forces a download. Use it
|
|
69
|
+
for documents and exports, never to show a web page.
|
|
70
|
+
- **exe.dev forwards ports 3000–9999** to `https://<vm>.exe.xyz:<port>/`, but a
|
|
71
|
+
VM has exactly **one public port** and the agent's webhook already owns it.
|
|
72
|
+
Alternate ports are account-gated: fine for the client reviewing work, not for
|
|
73
|
+
the public.
|
|
74
|
+
- **Anything genuinely public needs a deploy target** — the client's own Vercel
|
|
75
|
+
token, or their hosting. Treat "public" as a deploy step, not a toggle.
|
|
76
|
+
- A sandbox is a container: its ports are not reachable from the host, so a dev
|
|
77
|
+
server inside it cannot be previewed directly. Copy the artifact out (the
|
|
78
|
+
`preview` tool in `@kybernesis/exe`) or deploy it.
|
|
79
|
+
|
|
80
|
+
## Credential checklist — collect ALL of these from the client
|
|
81
|
+
|
|
82
|
+
Nothing here can be borrowed from another agent or another account.
|
|
83
|
+
|
|
84
|
+
1. **Host** — VM/server, plus the platform token if the agent provisions anything
|
|
85
|
+
2. **Model source** — their LLM API key, gateway allocation, or connected
|
|
86
|
+
subscription (exe: `integrations setup chatgpt`, then `integrations edit llm`)
|
|
87
|
+
3. **Channel app** — their Slack app (bot + app token) / Photon project / bot token
|
|
88
|
+
4. **Arcana** — workspaces + scoped `kb_` keys (one per brain, plus `-eval`)
|
|
89
|
+
5. **Storage for deliverables** — their blob store, or a served host directory
|
|
90
|
+
6. **Deploy target** — their Vercel token or hosting, if the agent ships sites
|
|
91
|
+
7. **Control plane** — agent registered and the pilot cohort granted
|
|
92
|
+
|
|
93
|
+
## Before calling it done
|
|
94
|
+
|
|
95
|
+
`kyb doctor` green (or every warning consciously accepted), the eval suite green
|
|
96
|
+
against the client's `-eval` workspace, and a live turn on the real surface.
|