@kybernesis/create 0.13.4 → 0.14.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 +6 -0
- package/dist/add.d.ts +6 -0
- package/dist/add.js +32 -1
- package/dist/cli.js +1 -0
- package/dist/credential.js +5 -3
- package/dist/docker-templates.d.ts +46 -0
- package/dist/docker-templates.js +254 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +61 -11
- package/dist/host-artifacts.d.ts +24 -0
- package/dist/host-artifacts.js +57 -0
- package/dist/init.js +15 -9
- package/dist/removed-default-tools.d.ts +29 -0
- package/dist/removed-default-tools.js +86 -0
- package/dist/sandbox-cleanup.d.ts +18 -0
- package/dist/sandbox-cleanup.js +57 -0
- package/dist/systemd.d.ts +41 -0
- package/dist/systemd.js +136 -0
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +53 -2
- package/dist/upgrade.js +135 -43
- package/dist/util.d.ts +1 -1
- package/dist/util.js +1 -1
- package/package.json +3 -2
- package/skills/fde-engagement/references/playbook.md +3 -3
- package/skills/kybernesis-packages/SKILL.md +5 -2
package/dist/init.js
CHANGED
|
@@ -5,6 +5,8 @@ import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEv
|
|
|
5
5
|
import { suiteDir } from "./skills.js";
|
|
6
6
|
import { configureArcana } from "./arcana.js";
|
|
7
7
|
import { upsertEnv } from "./envfile.js";
|
|
8
|
+
import { systemdRestartCommand } from "./systemd.js";
|
|
9
|
+
import { sandboxCleanupScaffoldFiles } from "./sandbox-cleanup.js";
|
|
8
10
|
/**
|
|
9
11
|
* The always-installed core. Everything else — channels, subagents, engineer,
|
|
10
12
|
* host bindings — is opt-in, because assuming them means the FDE deletes files
|
|
@@ -252,24 +254,23 @@ export async function init(rawName, options = {}) {
|
|
|
252
254
|
console.log(yellow(" ! could not install scripts/claude-subscription.sh — copy it from node_modules/@kybernesis/exe/scripts/"));
|
|
253
255
|
}
|
|
254
256
|
/**
|
|
255
|
-
* Point
|
|
257
|
+
* Point management restarts at the one supervisor that owns production.
|
|
256
258
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* then reports that a restart is still required, which reads as a broken
|
|
262
|
-
* feature rather than one line of config nobody was asked for.
|
|
259
|
+
* install-service.sh installs systemd with a build-before-start gate. Using
|
|
260
|
+
* eve-server.sh after that would create a second supervisor and can leave
|
|
261
|
+
* two executors racing over one durable store. `-n` makes missing sudo
|
|
262
|
+
* authorization fail instead of hanging a detached Studio restart.
|
|
263
263
|
*/
|
|
264
264
|
const manageChannelFile = join(dir, "agent/channels/kyb.ts");
|
|
265
265
|
if (existsSync(manageChannelFile)) {
|
|
266
266
|
const appRoot = `/home/exedev/${name}`;
|
|
267
|
+
const restartCommand = systemdRestartCommand(name);
|
|
267
268
|
const wired = readFileSync(manageChannelFile, "utf8").replace(/export default manageChannel\(\{[\s\S]*?\}\);/, () => `export default manageChannel({\n` +
|
|
268
269
|
` appRoot: process.env.EVE_APP_DIR ?? ${JSON.stringify(appRoot)},\n` +
|
|
269
|
-
` restartCommand:
|
|
270
|
+
` restartCommand: ${JSON.stringify(restartCommand)},\n` +
|
|
270
271
|
`});`);
|
|
271
272
|
writeFileSync(manageChannelFile, wired);
|
|
272
|
-
console.log(dim(
|
|
273
|
+
console.log(dim(` agent/channels/kyb.ts — restart wired to systemd (${restartCommand})`));
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
if (plan.file) {
|
|
@@ -297,6 +298,11 @@ export async function init(rawName, options = {}) {
|
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
}
|
|
301
|
+
for (const file of sandboxCleanupScaffoldFiles(host, depts)) {
|
|
302
|
+
const full = join(dir, file.path);
|
|
303
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
304
|
+
writeFileSync(full, file.content);
|
|
305
|
+
}
|
|
300
306
|
if (engPlan) {
|
|
301
307
|
for (const file of engPlan.files) {
|
|
302
308
|
const full = join(dir, file.path);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eve 0.39 removed `glob` and `grep` from the default tool set (changelog
|
|
3
|
+
* 4c1bd80). Two things follow for an agent written against 0.38:
|
|
4
|
+
*
|
|
5
|
+
* - A `tools/glob.ts` or `tools/grep.ts` that exports `disableTool()` now
|
|
6
|
+
* disables a slot nothing provides. On 0.38 that compiled with a warning;
|
|
7
|
+
* from 0.39 it is a compile error — the whole agent fails to build, and
|
|
8
|
+
* `eve eval` exits 1 with that one line as the only output. Kyber's ten
|
|
9
|
+
* knowledge specialists each carried both files.
|
|
10
|
+
* - A scope that HAD the two tools by default (the root, the builder) loses
|
|
11
|
+
* them silently. The certified agent's behaviour is preserved by opting
|
|
12
|
+
* those scopes back in explicitly, which eve supports with a one-line file.
|
|
13
|
+
*
|
|
14
|
+
* Both happen in one pass over every agent scope, and only in the direction
|
|
15
|
+
* that preserves what the agent already did.
|
|
16
|
+
*/
|
|
17
|
+
export declare const REMOVED_DEFAULT_TOOLS: readonly ["glob", "grep"];
|
|
18
|
+
/**
|
|
19
|
+
* A scope that disables `bash` has opted out of the sandbox altogether (the
|
|
20
|
+
* five sandbox tools proxy into one container; disabling bash is how a
|
|
21
|
+
* knowledge specialist stays container-free). Such a scope never had a
|
|
22
|
+
* reason to keep glob or grep either.
|
|
23
|
+
*/
|
|
24
|
+
export declare function scopeHasSandboxTools(scope: string): boolean;
|
|
25
|
+
export interface RemovedDefaultToolsRepair {
|
|
26
|
+
removed: string[];
|
|
27
|
+
optedIn: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function repairRemovedDefaultTools(cwd: string): RemovedDefaultToolsRepair;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* eve 0.39 removed `glob` and `grep` from the default tool set (changelog
|
|
5
|
+
* 4c1bd80). Two things follow for an agent written against 0.38:
|
|
6
|
+
*
|
|
7
|
+
* - A `tools/glob.ts` or `tools/grep.ts` that exports `disableTool()` now
|
|
8
|
+
* disables a slot nothing provides. On 0.38 that compiled with a warning;
|
|
9
|
+
* from 0.39 it is a compile error — the whole agent fails to build, and
|
|
10
|
+
* `eve eval` exits 1 with that one line as the only output. Kyber's ten
|
|
11
|
+
* knowledge specialists each carried both files.
|
|
12
|
+
* - A scope that HAD the two tools by default (the root, the builder) loses
|
|
13
|
+
* them silently. The certified agent's behaviour is preserved by opting
|
|
14
|
+
* those scopes back in explicitly, which eve supports with a one-line file.
|
|
15
|
+
*
|
|
16
|
+
* Both happen in one pass over every agent scope, and only in the direction
|
|
17
|
+
* that preserves what the agent already did.
|
|
18
|
+
*/
|
|
19
|
+
export const REMOVED_DEFAULT_TOOLS = ["glob", "grep"];
|
|
20
|
+
function optInSource(tool) {
|
|
21
|
+
return (`// eve 0.39 removed ${tool} from the default tool set. This scope had it on the\n` +
|
|
22
|
+
`// certified 0.38 stack, so it is opted back in explicitly (kyb upgrade wrote this).\n` +
|
|
23
|
+
`export { default } from "eve/tools/${tool}";\n`);
|
|
24
|
+
}
|
|
25
|
+
function isDisableToolFile(path) {
|
|
26
|
+
try {
|
|
27
|
+
return /disableTool\s*\(/.test(readFileSync(path, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function agentScopes(cwd) {
|
|
34
|
+
const root = join(cwd, "agent");
|
|
35
|
+
if (!existsSync(root))
|
|
36
|
+
return [];
|
|
37
|
+
const scopes = [];
|
|
38
|
+
const visit = (dir) => {
|
|
39
|
+
if (existsSync(join(dir, "agent.ts")))
|
|
40
|
+
scopes.push(dir);
|
|
41
|
+
const subagents = join(dir, "subagents");
|
|
42
|
+
if (!existsSync(subagents))
|
|
43
|
+
return;
|
|
44
|
+
for (const entry of readdirSync(subagents, { withFileTypes: true })) {
|
|
45
|
+
if (entry.isDirectory())
|
|
46
|
+
visit(join(subagents, entry.name));
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
visit(root);
|
|
50
|
+
return scopes;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A scope that disables `bash` has opted out of the sandbox altogether (the
|
|
54
|
+
* five sandbox tools proxy into one container; disabling bash is how a
|
|
55
|
+
* knowledge specialist stays container-free). Such a scope never had a
|
|
56
|
+
* reason to keep glob or grep either.
|
|
57
|
+
*/
|
|
58
|
+
export function scopeHasSandboxTools(scope) {
|
|
59
|
+
const bash = join(scope, "tools", "bash.ts");
|
|
60
|
+
return !(existsSync(bash) && isDisableToolFile(bash));
|
|
61
|
+
}
|
|
62
|
+
export function repairRemovedDefaultTools(cwd) {
|
|
63
|
+
const removed = [];
|
|
64
|
+
const optedIn = [];
|
|
65
|
+
for (const scope of agentScopes(cwd)) {
|
|
66
|
+
const tools = join(scope, "tools");
|
|
67
|
+
for (const tool of REMOVED_DEFAULT_TOOLS) {
|
|
68
|
+
const file = join(tools, `${tool}.ts`);
|
|
69
|
+
if (existsSync(file)) {
|
|
70
|
+
if (isDisableToolFile(file)) {
|
|
71
|
+
unlinkSync(file);
|
|
72
|
+
removed.push(relative(cwd, file));
|
|
73
|
+
}
|
|
74
|
+
// Anything else authored there (an opt-in, a custom tool) is the
|
|
75
|
+
// agent's own and stays.
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!scopeHasSandboxTools(scope))
|
|
79
|
+
continue;
|
|
80
|
+
mkdirSync(tools, { recursive: true });
|
|
81
|
+
writeFileSync(file, optInSource(tool));
|
|
82
|
+
optedIn.push(relative(cwd, file));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { removed, optedIn };
|
|
86
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const SANDBOX_CLEANUP_HOOK_FILE = "sandbox-cleanup.ts";
|
|
2
|
+
export declare function terminalSandboxCleanupHookTs(): string;
|
|
3
|
+
export declare function sandboxCleanupScaffoldFiles(host: "vercel" | "exe", subagents: string[]): Array<{
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Add the dedicated managed hook where it is missing, without replacing an
|
|
9
|
+
* authored file that happens to use the same path.
|
|
10
|
+
*
|
|
11
|
+
* Only scopes that can have a sandbox get it. `ctx.getSandbox()` PROVISIONS
|
|
12
|
+
* a sandbox when the session has none (eve docs, "Delete a sandbox": the next
|
|
13
|
+
* call provisions a fresh workspace), so a cleanup hook on a scope that has
|
|
14
|
+
* disabled its sandbox tools would start a container at session end in order
|
|
15
|
+
* to delete it — on Kyber, ten knowledge specialists doing that per closed
|
|
16
|
+
* session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function repairTerminalSandboxCleanupHooks(cwd: string): string[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { scopeHasSandboxTools } from "./removed-default-tools.js";
|
|
4
|
+
export const SANDBOX_CLEANUP_HOOK_FILE = "sandbox-cleanup.ts";
|
|
5
|
+
export function terminalSandboxCleanupHookTs() {
|
|
6
|
+
return `export { terminalSandboxCleanupHook as default } from "@kybernesis/exe/sandbox-cleanup";\n`;
|
|
7
|
+
}
|
|
8
|
+
export function sandboxCleanupScaffoldFiles(host, subagents) {
|
|
9
|
+
if (host !== "exe")
|
|
10
|
+
return [];
|
|
11
|
+
return ["agent", ...subagents.map((name) => join("agent/subagents", name))].map((scope) => ({
|
|
12
|
+
path: join(scope, "hooks", SANDBOX_CLEANUP_HOOK_FILE),
|
|
13
|
+
content: terminalSandboxCleanupHookTs(),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function findLocalSubagents(directory) {
|
|
17
|
+
if (!existsSync(directory))
|
|
18
|
+
return [];
|
|
19
|
+
const found = [];
|
|
20
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
21
|
+
if (!entry.isDirectory())
|
|
22
|
+
continue;
|
|
23
|
+
const child = join(directory, entry.name);
|
|
24
|
+
if (existsSync(join(child, "agent.ts")))
|
|
25
|
+
found.push(child);
|
|
26
|
+
found.push(...findLocalSubagents(child));
|
|
27
|
+
}
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Add the dedicated managed hook where it is missing, without replacing an
|
|
32
|
+
* authored file that happens to use the same path.
|
|
33
|
+
*
|
|
34
|
+
* Only scopes that can have a sandbox get it. `ctx.getSandbox()` PROVISIONS
|
|
35
|
+
* a sandbox when the session has none (eve docs, "Delete a sandbox": the next
|
|
36
|
+
* call provisions a fresh workspace), so a cleanup hook on a scope that has
|
|
37
|
+
* disabled its sandbox tools would start a container at session end in order
|
|
38
|
+
* to delete it — on Kyber, ten knowledge specialists doing that per closed
|
|
39
|
+
* session.
|
|
40
|
+
*/
|
|
41
|
+
export function repairTerminalSandboxCleanupHooks(cwd) {
|
|
42
|
+
const agent = join(cwd, "agent");
|
|
43
|
+
if (!existsSync(agent))
|
|
44
|
+
return [];
|
|
45
|
+
const scopes = [agent, ...findLocalSubagents(join(agent, "subagents"))].filter(scopeHasSandboxTools);
|
|
46
|
+
const added = [];
|
|
47
|
+
for (const scope of scopes) {
|
|
48
|
+
const hooks = join(scope, "hooks");
|
|
49
|
+
const target = join(hooks, SANDBOX_CLEANUP_HOOK_FILE);
|
|
50
|
+
if (existsSync(target))
|
|
51
|
+
continue;
|
|
52
|
+
mkdirSync(hooks, { recursive: true });
|
|
53
|
+
writeFileSync(target, terminalSandboxCleanupHookTs());
|
|
54
|
+
added.push(target);
|
|
55
|
+
}
|
|
56
|
+
return added;
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const LEGACY_RESTART_COMMAND = "bash scripts/eve-server.sh restart";
|
|
2
|
+
export declare function systemdRestartCommand(name: string): string;
|
|
3
|
+
export interface ManageRestartDiagnosis {
|
|
4
|
+
verdict: "pass" | "warn" | "fail";
|
|
5
|
+
label: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function diagnoseManageRestart(serviceName: string | null, restartCommand: string | undefined): ManageRestartDiagnosis;
|
|
9
|
+
export interface UnitValues {
|
|
10
|
+
name: string;
|
|
11
|
+
user: string;
|
|
12
|
+
app: string;
|
|
13
|
+
port: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseAgentServiceUnit(path: string, contents: string): UnitValues | null;
|
|
16
|
+
export type RestartMigration = "absent" | "missing" | "migrated" | "systemd" | "custom" | "blocked";
|
|
17
|
+
export declare function reconcileManageRestart(cwd: string, name: string): RestartMigration;
|
|
18
|
+
export interface InstalledAgentService {
|
|
19
|
+
path: string;
|
|
20
|
+
contents: string;
|
|
21
|
+
values: UnitValues;
|
|
22
|
+
mode: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function findMatchingAgentServiceUnit(cwd: string, systemdDir?: string): InstalledAgentService | null;
|
|
25
|
+
/**
|
|
26
|
+
* Refresh an existing package-generated unit. Deliberately never creates a
|
|
27
|
+
* missing unit, enables it, or restarts it; upgrade only reconciles artifacts
|
|
28
|
+
* whose systemd ownership is already established.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Once systemd owns the agent, a Studio install that restarts through
|
|
32
|
+
* scripts/eve-server.sh starts a second supervisor beside it: the script
|
|
33
|
+
* kills systemd's child and starts its own while Restart=always starts
|
|
34
|
+
* another, and two executors race over one durable store. Migrate the exact
|
|
35
|
+
* scaffolded literal in agent/channels/kyb.ts to the systemd restart; leave
|
|
36
|
+
* anything an operator wrote, with the exact line to use.
|
|
37
|
+
*
|
|
38
|
+
* The unit itself is package-managed by repairHostArtifacts (managed marker,
|
|
39
|
+
* .bak copy); this only follows what that unit implies for restarts.
|
|
40
|
+
*/
|
|
41
|
+
export declare function repairManageRestart(cwd: string, deps: Record<string, string>, systemdDir?: string): RestartMigration | null;
|
package/dist/systemd.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { dim, green, yellow } from "./util.js";
|
|
4
|
+
export const LEGACY_RESTART_COMMAND = "bash scripts/eve-server.sh restart";
|
|
5
|
+
export function systemdRestartCommand(name) {
|
|
6
|
+
return `sudo -n systemctl restart ${name}-agent`;
|
|
7
|
+
}
|
|
8
|
+
export function diagnoseManageRestart(serviceName, restartCommand) {
|
|
9
|
+
if (!serviceName) {
|
|
10
|
+
return restartCommand
|
|
11
|
+
? { verdict: "pass", label: "management routes can restart the agent after an install" }
|
|
12
|
+
: {
|
|
13
|
+
verdict: "warn",
|
|
14
|
+
label: "management routes have no restartCommand",
|
|
15
|
+
detail: "installing edits this repo and rebuilds; without a restart the response says a restart is still required. Install the systemd service, then use the exact restartCommand its installer prints",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const desired = systemdRestartCommand(serviceName);
|
|
19
|
+
const remediation = `set restartCommand in agent/channels/kyb.ts to ${JSON.stringify(desired)}. ` +
|
|
20
|
+
`Verify passwordless sudo with: ${desired}. systemctl restart does not wait for an in-flight turn; an install can interrupt it.`;
|
|
21
|
+
if (restartCommand === LEGACY_RESTART_COMMAND) {
|
|
22
|
+
return {
|
|
23
|
+
verdict: "fail",
|
|
24
|
+
label: "management restart uses eve-server.sh while systemd owns the agent",
|
|
25
|
+
detail: `this can start two servers against one durable store; ${remediation}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (restartCommand === desired) {
|
|
29
|
+
return { verdict: "pass", label: `management routes restart the systemd owner (${serviceName}-agent)` };
|
|
30
|
+
}
|
|
31
|
+
if (restartCommand) {
|
|
32
|
+
return {
|
|
33
|
+
verdict: "warn",
|
|
34
|
+
label: `management restartCommand is customized and was preserved (${JSON.stringify(restartCommand)})`,
|
|
35
|
+
detail: `confirm it cannot start a second supervisor; ${remediation}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { verdict: "warn", label: "management routes have no restartCommand", detail: remediation };
|
|
39
|
+
}
|
|
40
|
+
export function parseAgentServiceUnit(path, contents) {
|
|
41
|
+
const fileName = /^(.+)-agent\.service$/.exec(basename(path))?.[1];
|
|
42
|
+
const user = /^User=(.+)$/m.exec(contents)?.[1];
|
|
43
|
+
const app = /^WorkingDirectory=(.+)$/m.exec(contents)?.[1];
|
|
44
|
+
const port = /^Environment=PORT=(.+)$/m.exec(contents)?.[1];
|
|
45
|
+
if (!fileName || !user || !app || !port)
|
|
46
|
+
return null;
|
|
47
|
+
return { name: fileName, user, app, port };
|
|
48
|
+
}
|
|
49
|
+
export function reconcileManageRestart(cwd, name) {
|
|
50
|
+
const path = join(cwd, "agent/channels/kyb.ts");
|
|
51
|
+
let before;
|
|
52
|
+
try {
|
|
53
|
+
before = readFileSync(path, "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return "absent";
|
|
57
|
+
}
|
|
58
|
+
const property = /\brestartCommand\s*:\s*(["'`])([^"'`]+)\1/m.exec(before);
|
|
59
|
+
const configured = property?.[2];
|
|
60
|
+
if (!configured)
|
|
61
|
+
return "missing";
|
|
62
|
+
if (configured === systemdRestartCommand(name))
|
|
63
|
+
return "systemd";
|
|
64
|
+
if (configured !== LEGACY_RESTART_COMMAND || property?.[1] !== '"')
|
|
65
|
+
return "custom";
|
|
66
|
+
try {
|
|
67
|
+
const replacement = property[0].replace(`${property[1]}${configured}${property[1]}`, JSON.stringify(systemdRestartCommand(name)));
|
|
68
|
+
writeFileSync(path, before.slice(0, property.index) + replacement + before.slice(property.index + property[0].length));
|
|
69
|
+
return "migrated";
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return "blocked";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function findMatchingAgentServiceUnit(cwd, systemdDir = "/etc/systemd/system") {
|
|
76
|
+
try {
|
|
77
|
+
for (const file of readdirSync(systemdDir).filter((entry) => entry.endsWith("-agent.service"))) {
|
|
78
|
+
const path = join(systemdDir, file);
|
|
79
|
+
const contents = readFileSync(path, "utf8");
|
|
80
|
+
const values = parseAgentServiceUnit(path, contents);
|
|
81
|
+
if (values && resolve(values.app) === resolve(cwd)) {
|
|
82
|
+
return { path, contents, values, mode: statSync(path).mode & 0o777 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function printRestartGuidance(state, cwd, name) {
|
|
92
|
+
if (state === "absent" || state === "systemd")
|
|
93
|
+
return;
|
|
94
|
+
const desired = systemdRestartCommand(name);
|
|
95
|
+
if (state === "migrated") {
|
|
96
|
+
console.log(` ${green("+")} migrated agent/channels/kyb.ts to ${desired}`);
|
|
97
|
+
}
|
|
98
|
+
else if (state === "custom" || state === "blocked") {
|
|
99
|
+
console.log(` ${yellow("!")} ${state === "blocked" ? "could not migrate" : "preserved customized"} restartCommand in agent/channels/kyb.ts. systemd owns ` +
|
|
100
|
+
`this agent; scripts/eve-server.sh would create a second supervisor. Review it and use exactly:\n` +
|
|
101
|
+
` ${dim(`restartCommand: ${JSON.stringify(desired)},`)}\n` +
|
|
102
|
+
` ${dim(`Verify passwordless sudo with: ${desired}`)}\n` +
|
|
103
|
+
` ${dim("systemctl restart does not wait for an in-flight turn; an install can interrupt it.")}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` ${yellow("!")} ${join(cwd, "agent/channels/kyb.ts")} has no restartCommand. Add exactly:\n` +
|
|
107
|
+
` ${dim(`restartCommand: ${JSON.stringify(desired)},`)}\n` +
|
|
108
|
+
` ${dim("It needs passwordless sudo and can interrupt an in-flight turn.")}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Refresh an existing package-generated unit. Deliberately never creates a
|
|
113
|
+
* missing unit, enables it, or restarts it; upgrade only reconciles artifacts
|
|
114
|
+
* whose systemd ownership is already established.
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Once systemd owns the agent, a Studio install that restarts through
|
|
118
|
+
* scripts/eve-server.sh starts a second supervisor beside it: the script
|
|
119
|
+
* kills systemd's child and starts its own while Restart=always starts
|
|
120
|
+
* another, and two executors race over one durable store. Migrate the exact
|
|
121
|
+
* scaffolded literal in agent/channels/kyb.ts to the systemd restart; leave
|
|
122
|
+
* anything an operator wrote, with the exact line to use.
|
|
123
|
+
*
|
|
124
|
+
* The unit itself is package-managed by repairHostArtifacts (managed marker,
|
|
125
|
+
* .bak copy); this only follows what that unit implies for restarts.
|
|
126
|
+
*/
|
|
127
|
+
export function repairManageRestart(cwd, deps, systemdDir = "/etc/systemd/system") {
|
|
128
|
+
if (!deps["@kybernesis/exe"])
|
|
129
|
+
return null;
|
|
130
|
+
const match = findMatchingAgentServiceUnit(cwd, systemdDir);
|
|
131
|
+
if (!match)
|
|
132
|
+
return null;
|
|
133
|
+
const state = reconcileManageRestart(cwd, match.values.name);
|
|
134
|
+
printRestartGuidance(state, cwd, match.values.name);
|
|
135
|
+
return state;
|
|
136
|
+
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -50,3 +50,15 @@ export declare function engineerPlan(host: HostKind, model: string): EngineerPla
|
|
|
50
50
|
* endpoint, so none of the subscription backend's constraints apply.
|
|
51
51
|
*/
|
|
52
52
|
export declare function exeEvalConfigTs(): string;
|
|
53
|
+
/**
|
|
54
|
+
* An eve 0.49 memory slot backed by Arcana.
|
|
55
|
+
*
|
|
56
|
+
* Written by `kyb add memory`. The slot gives the agent recall before every
|
|
57
|
+
* turn without a skill telling the model to go and look; capture stays off
|
|
58
|
+
* because the remember skill already stores facts deliberately. When the
|
|
59
|
+
* Arcana extension is mounted the slot offers no tools of its own — the
|
|
60
|
+
* extension's `arcana_*` set is the same brain through the same key.
|
|
61
|
+
*/
|
|
62
|
+
export declare function arcanaMemorySlotTs(options: {
|
|
63
|
+
extensionMounted: boolean;
|
|
64
|
+
}): string;
|
package/dist/templates.js
CHANGED
|
@@ -383,8 +383,9 @@ export function hostSteps(host, name) {
|
|
|
383
383
|
`Attach an LLM integration (ChatGPT subscription, your API key, or exe's gateway):`,
|
|
384
384
|
` ssh exe.dev integrations setup chatgpt --name work # once, device-code`,
|
|
385
385
|
` ssh exe.dev integrations edit llm --openai=chatgpt --openai-account=work`,
|
|
386
|
-
`Install Node 24 + deps on the VM,
|
|
387
|
-
|
|
386
|
+
`Install Node 24 + deps on the VM, fill .env.local, then install the production service:`,
|
|
387
|
+
` bash node_modules/@kybernesis/exe/scripts/install-service.sh`,
|
|
388
|
+
`The systemd unit exports .env.local, builds before every start, and is the single supervisor`,
|
|
388
389
|
];
|
|
389
390
|
}
|
|
390
391
|
return [
|
|
@@ -571,6 +572,12 @@ export { default } from "@kybernesis/engineer";
|
|
|
571
572
|
content: workshopSandbox(host),
|
|
572
573
|
},
|
|
573
574
|
];
|
|
575
|
+
if (onExe) {
|
|
576
|
+
files.push({
|
|
577
|
+
path: "agent/subagents/builder/hooks/sandbox-cleanup.ts",
|
|
578
|
+
content: `export { terminalSandboxCleanupHook as default } from "@kybernesis/exe/sandbox-cleanup";\n`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
574
581
|
if (onExe) {
|
|
575
582
|
files.push({
|
|
576
583
|
path: "agent/subagents/builder/tools/preview.ts",
|
|
@@ -627,3 +634,47 @@ export default defineEvalConfig({
|
|
|
627
634
|
});
|
|
628
635
|
`;
|
|
629
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* An eve 0.49 memory slot backed by Arcana.
|
|
639
|
+
*
|
|
640
|
+
* Written by `kyb add memory`. The slot gives the agent recall before every
|
|
641
|
+
* turn without a skill telling the model to go and look; capture stays off
|
|
642
|
+
* because the remember skill already stores facts deliberately. When the
|
|
643
|
+
* Arcana extension is mounted the slot offers no tools of its own — the
|
|
644
|
+
* extension's `arcana_*` set is the same brain through the same key.
|
|
645
|
+
*/
|
|
646
|
+
export function arcanaMemorySlotTs(options) {
|
|
647
|
+
return `import { defineMemory } from "eve/memory";
|
|
648
|
+
import { byPrincipal } from "eve/memory/scope";
|
|
649
|
+
import { arcanaMemory } from "@kybernesis/arcana/memory";
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Recall before every turn, from the company brain. eve resolves who the
|
|
653
|
+
* memory belongs to (byPrincipal: the authenticated caller; local-dev while
|
|
654
|
+
* developing) and delivers what Arcana finds as one message the model sees
|
|
655
|
+
* before it answers. Greetings and other short turns skip recall.
|
|
656
|
+
*
|
|
657
|
+
* Capture is off: this agent remembers deliberately through arcana_remember.
|
|
658
|
+
* Set capture: { enabled: true } for an agent that should learn passively.
|
|
659
|
+
*/
|
|
660
|
+
const COMPANY = process.env.ARCANA_COMPANY_WORKSPACE!;
|
|
661
|
+
const DM = process.env.ARCANA_DM_WORKSPACE ?? COMPANY;
|
|
662
|
+
|
|
663
|
+
export default defineMemory({
|
|
664
|
+
description: "Durable company memory in Arcana: people, decisions, projects, preferences.",
|
|
665
|
+
provider: arcanaMemory({
|
|
666
|
+
// Keys are workspace-scoped. The eval script points every workspace at the
|
|
667
|
+
// -eval brain, which has its own key; the same choice the extension makes.
|
|
668
|
+
apiKey:
|
|
669
|
+
(COMPANY.endsWith("-eval") ? process.env.ARCANA_EVAL_API_KEY : undefined) ??
|
|
670
|
+
process.env.ARCANA_API_KEY!,
|
|
671
|
+
workspace: COMPANY,
|
|
672
|
+
// A DM is a private surface with its own brain; verified from session auth,
|
|
673
|
+
// never from the message.
|
|
674
|
+
resolveWorkspace: (ctx) =>
|
|
675
|
+
ctx.session.auth.current?.attributes.surface === "dm" ? DM : undefined,${options.extensionMounted ? "\n // The Arcana extension already mounts the full arcana_* tool set.\n tools: false," : ""}
|
|
676
|
+
}),
|
|
677
|
+
scope: byPrincipal,
|
|
678
|
+
});
|
|
679
|
+
`;
|
|
680
|
+
}
|