@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 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[];
@@ -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
- const CORE_ITEMS = ["enterprise", "arcana", "evals"];
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.15.7",
4
- "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
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