@kybernesis/create 0.15.7 → 0.16.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/doctor.js +31 -0
- package/dist/env-urls.d.ts +17 -0
- package/dist/env-urls.js +79 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.js +49 -1
- package/package.json +4 -3
- package/skills/self-hosting/SKILL.md +25 -0
package/dist/doctor.js
CHANGED
|
@@ -9,6 +9,7 @@ import { classifyModelReach } from "./model-reach.js";
|
|
|
9
9
|
import { countAbandonedRuns } from "./run-store.js";
|
|
10
10
|
import { describeStaleSidecar, newestPackageChange, staleSidecars } from "./stale-sidecar.js";
|
|
11
11
|
import { checkSelfVersion } from "./self-version.js";
|
|
12
|
+
import { envUrlProblems } from "./env-urls.js";
|
|
12
13
|
const MARK = {
|
|
13
14
|
pass: green("✓"),
|
|
14
15
|
warn: yellow("!"),
|
|
@@ -215,6 +216,22 @@ export async function doctor() {
|
|
|
215
216
|
};
|
|
216
217
|
if (!existsSync(envPath))
|
|
217
218
|
add("warn", ".env.local missing", "copy .env.example and fill it");
|
|
219
|
+
/**
|
|
220
|
+
* Config that is shaped wrong rather than missing.
|
|
221
|
+
*
|
|
222
|
+
* A URL-valued setting with no scheme does not fail at start; it fails inside
|
|
223
|
+
* whatever background drain uses it, once per turn, forever. The reference
|
|
224
|
+
* fleet logged 121 of these and lost every analytics event of a deployment
|
|
225
|
+
* while reporting healthy throughout. The value alone is enough to know, so
|
|
226
|
+
* this costs nothing and lands before a client's first turn.
|
|
227
|
+
*/
|
|
228
|
+
const urlProblems = existsSync(envPath) ? envUrlProblems(readFileSync(envPath, "utf8")) : [];
|
|
229
|
+
for (const problem of urlProblems) {
|
|
230
|
+
add("fail", `${problem.name} cannot build a request (${problem.reason})`, problem.fix);
|
|
231
|
+
}
|
|
232
|
+
if (existsSync(envPath) && urlProblems.length === 0) {
|
|
233
|
+
add("pass", "every URL-valued setting is an absolute http(s) URL");
|
|
234
|
+
}
|
|
218
235
|
const githubCheck = githubToolsDoctorCheck(existsSync(join(cwd, "agent/extensions/github.ts")), env);
|
|
219
236
|
if (githubCheck)
|
|
220
237
|
add(githubCheck.verdict, githubCheck.label, githubCheck.detail);
|
|
@@ -363,6 +380,20 @@ export async function doctor() {
|
|
|
363
380
|
add("warn", "@kybernesis/dispatch installed but no edges or dispatch channel found", "see the connect-agents skill");
|
|
364
381
|
}
|
|
365
382
|
}
|
|
383
|
+
// ── .agent identity (ARP) ──────────────────────────────────────────────
|
|
384
|
+
// The owner connects the agent from the ARP console; the only thing that
|
|
385
|
+
// has to be true in this repo is that deliveries get in.
|
|
386
|
+
if (deps["@kybernesis/identity"]) {
|
|
387
|
+
if (!existsSync(join(cwd, "agent/channels/arp.ts")))
|
|
388
|
+
add("warn", "identity: agent/channels/arp.ts missing", "npx eve add @kybernesis/identity (serves /eve/v1/arp for Connect your agent)");
|
|
389
|
+
if (eveChannelSrc && !eveChannelSrc.includes("arpAuth("))
|
|
390
|
+
add("fail", "identity: eve channel has no arpAuth()", "add arpAuth() to the channel's auth list — without it every delivery from a paired agent is a 401");
|
|
391
|
+
else if (eveChannelSrc)
|
|
392
|
+
add("pass", "identity: eve channel accepts ARP deliveries (arpAuth)");
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
add("warn", "no .agent identity add-on", "npx eve add @kybernesis/identity — lets the owner connect this agent to a .agent name from the console");
|
|
396
|
+
}
|
|
366
397
|
// ── self-hosted agents (host !== Vercel) ───────────────────────────────
|
|
367
398
|
// Every check here cost a real debugging session on the first exe.dev
|
|
368
399
|
// deployment. None of them are theoretical.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Does this name hold a whole URL? */
|
|
2
|
+
export declare function expectsUrl(name: string): boolean;
|
|
3
|
+
export type EnvUrlProblem = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** Why it cannot be used to build a request. Never includes the value. */
|
|
6
|
+
readonly reason: "no scheme" | "not a url" | "not http(s)";
|
|
7
|
+
readonly fix: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Check one value. Returns undefined when it is fine.
|
|
11
|
+
*
|
|
12
|
+
* The value never appears in the result: these files sit beside real secrets
|
|
13
|
+
* and a diagnostic that echoes them turns a config warning into a leak.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkEnvUrl(name: string, value: string): EnvUrlProblem | undefined;
|
|
16
|
+
/** Every URL-shaped value in this env text that cannot build a request. */
|
|
17
|
+
export declare function envUrlProblems(text: string): EnvUrlProblem[];
|
package/dist/env-urls.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parseEnv } from "./util.js";
|
|
2
|
+
/**
|
|
3
|
+
* Environment values that are used to build a URL, and whether they can be.
|
|
4
|
+
*
|
|
5
|
+
* An SDK handed `POSTHOG_HOST=eu.i.posthog.com` does not complain at start. It
|
|
6
|
+
* fails once per turn, forever, inside a background drain: `Failed to parse URL
|
|
7
|
+
* from eu.i.posthog.com/batch/`. On the reference fleet that was 121 logged
|
|
8
|
+
* failures and every analytics event of the deployment silently discarded,
|
|
9
|
+
* while the agent itself looked perfectly healthy. Nothing surfaces it because
|
|
10
|
+
* nothing that matters to the turn depends on it.
|
|
11
|
+
*
|
|
12
|
+
* Hence checking the SHAPE of the value at rest, which costs nothing and finds
|
|
13
|
+
* it before a client's first turn.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Names whose value is unambiguously a whole URL.
|
|
17
|
+
*
|
|
18
|
+
* `_HOST` is deliberately NOT here: plenty of them legitimately hold a bare
|
|
19
|
+
* hostname (`SMTP_HOST`, `PGHOST`, `REDIS_HOST`), and flagging those would make
|
|
20
|
+
* doctor cry wolf on a correct config.
|
|
21
|
+
*/
|
|
22
|
+
const URL_SUFFIXES = ["_URL", "_ENDPOINT", "_ISSUER"];
|
|
23
|
+
/**
|
|
24
|
+
* `_HOST` names that specific SDKs use as a base URL, so they do need a scheme.
|
|
25
|
+
* An allowlist rather than a rule, because only the SDK knows which it is.
|
|
26
|
+
*/
|
|
27
|
+
const BASE_URL_HOSTS = new Set(["POSTHOG_HOST"]);
|
|
28
|
+
/** Does this name hold a whole URL? */
|
|
29
|
+
export function expectsUrl(name) {
|
|
30
|
+
if (BASE_URL_HOSTS.has(name))
|
|
31
|
+
return true;
|
|
32
|
+
return URL_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check one value. Returns undefined when it is fine.
|
|
36
|
+
*
|
|
37
|
+
* The value never appears in the result: these files sit beside real secrets
|
|
38
|
+
* and a diagnostic that echoes them turns a config warning into a leak.
|
|
39
|
+
*/
|
|
40
|
+
export function checkEnvUrl(name, value) {
|
|
41
|
+
const trimmed = value.trim().replace(/^["']|["']$/g, "");
|
|
42
|
+
if (trimmed === "")
|
|
43
|
+
return undefined;
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = new URL(trimmed);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A bare host is by far the common case, and the remedy differs from junk.
|
|
50
|
+
const bare = /^[a-z0-9.-]+(:\d+)?(\/.*)?$/i.test(trimmed);
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
reason: bare ? "no scheme" : "not a url",
|
|
54
|
+
fix: bare
|
|
55
|
+
? `${name} has no scheme, so anything built from it fails to parse. Prefix it with https://`
|
|
56
|
+
: `${name} is not a URL. Set it to an absolute https:// URL.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
reason: "not http(s)",
|
|
63
|
+
fix: `${name} is a ${parsed.protocol.replace(":", "")} URL. Set it to an absolute https:// URL.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Every URL-shaped value in this env text that cannot build a request. */
|
|
69
|
+
export function envUrlProblems(text) {
|
|
70
|
+
const problems = [];
|
|
71
|
+
for (const [name, value] of Object.entries(parseEnv(text))) {
|
|
72
|
+
if (!expectsUrl(name))
|
|
73
|
+
continue;
|
|
74
|
+
const problem = checkEnvUrl(name, value);
|
|
75
|
+
if (problem)
|
|
76
|
+
problems.push(problem);
|
|
77
|
+
}
|
|
78
|
+
return problems;
|
|
79
|
+
}
|
package/dist/init.d.ts
CHANGED
|
@@ -39,3 +39,9 @@ export interface InitOptions {
|
|
|
39
39
|
yes?: boolean;
|
|
40
40
|
}
|
|
41
41
|
export declare function init(rawName: string | undefined, options?: InitOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Put `arpAuth()` first in the eve channel's auth walk so ARP Cloud deliveries
|
|
44
|
+
* are accepted. Returns the new source, the same source when already wired,
|
|
45
|
+
* or null when the file has no recognisable auth list.
|
|
46
|
+
*/
|
|
47
|
+
export declare function wireArpAuth(src: string): string | null;
|
package/dist/init.js
CHANGED
|
@@ -13,7 +13,9 @@ import { MODEL_REACH_ENV, resolveModelScaffold } from "./model-reach.js";
|
|
|
13
13
|
* host bindings — is opt-in, because assuming them means the FDE deletes files
|
|
14
14
|
* AND undoes real setup work (an Arcana workspace + scoped key per subagent).
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
// `identity` gives the agent its .agent name surface: the owner connects it
|
|
17
|
+
// from the ARP console with one click and never touches this repo again.
|
|
18
|
+
const CORE_ITEMS = ["enterprise", "arcana", "evals", "identity"];
|
|
17
19
|
// Official eve-registry limbs installed with the engineer subagent.
|
|
18
20
|
// connection/vercel is Vercel-Connect-backed, so it is VERCEL-HOST ONLY: on a
|
|
19
21
|
// self-hosted agent it cannot get an OIDC token and the agent fails to boot.
|
|
@@ -194,6 +196,20 @@ export async function init(rawName, options = {}) {
|
|
|
194
196
|
}
|
|
195
197
|
console.log(bold("\n3/6 Writing identity, model config, memory mount, and evals …"));
|
|
196
198
|
mkdirSync(join(dir, "agent/instructions"), { recursive: true });
|
|
199
|
+
// .agent identity: the channel + peers tool came from the registry item;
|
|
200
|
+
// deliveries need arpAuth() on the eve channel, and the model needs to know
|
|
201
|
+
// what the ask_<name>_agent tools are. Both are idempotent.
|
|
202
|
+
writeFileSync(join(dir, "agent/instructions/arp.md"), arpInstructionsMd());
|
|
203
|
+
{
|
|
204
|
+
const eveChannelPath = join(dir, "agent/channels/eve.ts");
|
|
205
|
+
if (existsSync(eveChannelPath)) {
|
|
206
|
+
const wired = wireArpAuth(readFileSync(eveChannelPath, "utf8"));
|
|
207
|
+
if (wired)
|
|
208
|
+
writeFileSync(eveChannelPath, wired);
|
|
209
|
+
else
|
|
210
|
+
console.log(yellow(" ! agent/channels/eve.ts: could not add arpAuth() — add it to the channel's auth list by hand"));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
197
213
|
writeFileSync(join(dir, "agent/instructions/identity.md"), identityMd(displayName, depts));
|
|
198
214
|
try {
|
|
199
215
|
unlinkSync(join(dir, "agent/instructions.md"));
|
|
@@ -398,3 +414,35 @@ ${steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
|
|
|
398
414
|
${dim(" Add more later: kyb add channel <kind> · kyb add subagent <name>")}
|
|
399
415
|
`);
|
|
400
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Put `arpAuth()` first in the eve channel's auth walk so ARP Cloud deliveries
|
|
419
|
+
* are accepted. Returns the new source, the same source when already wired,
|
|
420
|
+
* or null when the file has no recognisable auth list.
|
|
421
|
+
*/
|
|
422
|
+
export function wireArpAuth(src) {
|
|
423
|
+
if (src.includes("arpAuth("))
|
|
424
|
+
return src;
|
|
425
|
+
const lines = src.split("\n");
|
|
426
|
+
const lastImport = lines.reduce((acc, l, i) => (l.startsWith("import ") ? i : acc), -1);
|
|
427
|
+
lines.splice(lastImport + 1, 0, 'import { arpAuth } from "@kybernesis/identity";');
|
|
428
|
+
let out = lines.join("\n");
|
|
429
|
+
const comment = "// Deliveries from paired .agent peers: a per-message token ARP Cloud signs,\n // verified offline against its JWKS; skips to the next entry otherwise.";
|
|
430
|
+
if (/extraAuth:\s*\[/.test(out))
|
|
431
|
+
return out.replace(/extraAuth:\s*\[/, `extraAuth: [\n ${comment}\n arpAuth(),`);
|
|
432
|
+
if (/\bauth:\s*\[/.test(out))
|
|
433
|
+
return out.replace(/\bauth:\s*\[/, `auth: [\n ${comment}\n arpAuth(),`);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function arpInstructionsMd() {
|
|
437
|
+
return `## Your .agent identity
|
|
438
|
+
|
|
439
|
+
You have a registered name on the agent network. Other agents that are paired
|
|
440
|
+
with you appear as \`ask_<name>_agent\` tools (the name is their .agent name; a
|
|
441
|
+
plain \`ask_<name>\` tool, if present, is a different, non-network peer); use
|
|
442
|
+
them when a request belongs to that agent rather than to you. When a message
|
|
443
|
+
arrives from a paired agent, the caller's identity has already been verified
|
|
444
|
+
and the message has already been checked against what its owner allowed —
|
|
445
|
+
answer it as you would a trusted colleague, and honour any obligations
|
|
446
|
+
attached (for example, do not include fields you were told to redact).
|
|
447
|
+
`;
|
|
448
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent \u2014 plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://kybernesis.ai",
|
|
@@ -46,5 +46,6 @@
|
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {}
|
|
50
51
|
}
|
|
@@ -86,6 +86,31 @@ What this arrangement costs you, and it is worth saying to the client:
|
|
|
86
86
|
presents as the agent "breaking".
|
|
87
87
|
- **Ask the vendor's terms question before a client demo**, not after.
|
|
88
88
|
|
|
89
|
+
### A Claude subscription expires on a ~28-day clock, and lies on the way out
|
|
90
|
+
|
|
91
|
+
Claude is the third provider and behaves differently enough to plan around.
|
|
92
|
+
Grok and ChatGPT are a file a CLI keeps fresh; Claude is a **proxy process**
|
|
93
|
+
holding Claude Code's own OAuth (`scripts/claude-subscription.sh` in
|
|
94
|
+
`@kybernesis/exe`). Two things follow, both learned on Sid:
|
|
95
|
+
|
|
96
|
+
- **The sign-in has a hard expiry of about 28 days.** The access token refreshes
|
|
97
|
+
hourly and does so happily the whole time, which tells you nothing — the
|
|
98
|
+
*refresh* token underneath carries its own fixed expiry, and using it does not
|
|
99
|
+
extend it. On expiry the proxy blanks the credential file to empty strings and
|
|
100
|
+
every turn 401s. Only `login` recovers it; `reload` and restarts cannot.
|
|
101
|
+
**Diary the re-login for every subscription-backed agent you deploy.**
|
|
102
|
+
- **`/ready` cannot see a dead credential.** It reports that a credential was
|
|
103
|
+
*loaded*, not that it is *valid*, so it answers 200 over a blanked file while
|
|
104
|
+
the API 401s — `status` read "signed in, answering" straight through an
|
|
105
|
+
outage. `status` now probes `/v1/models` as well and warns from the refresh
|
|
106
|
+
expiry (`✓ sign-in valid for 27d`, escalating at 7 and 3 days). Run it on a
|
|
107
|
+
schedule; the failure is silent and looks like a model outage, so people go
|
|
108
|
+
and read Anthropic's status page instead of opening a browser.
|
|
109
|
+
|
|
110
|
+
Say the re-login cadence out loud to a client before it strands their agent at
|
|
111
|
+
a weekend. It is the one piece of operational debt this arrangement creates in
|
|
112
|
+
exchange for not billing metered API usage.
|
|
113
|
+
|
|
89
114
|
## The failure modes, each of which cost a real session
|
|
90
115
|
|
|
91
116
|
- **`EXE_MODEL` is deliberately empty after `kyb init`.** Set it from the
|