@vendoai/vendo 0.4.3 → 0.4.5

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.
Files changed (47) hide show
  1. package/dist/capability-misses.js +2 -3
  2. package/dist/cli/cloud/client.d.ts +2 -5
  3. package/dist/cli/cloud/client.js +4 -4
  4. package/dist/cli/dep-versions.d.ts +1 -1
  5. package/dist/cli/dep-versions.js +1 -1
  6. package/dist/cli/doctor-codes.d.ts +5 -0
  7. package/dist/cli/doctor-codes.js +5 -0
  8. package/dist/cli/doctor.d.ts +1 -0
  9. package/dist/cli/doctor.js +86 -2
  10. package/dist/cli/extract/apply.js +1 -1
  11. package/dist/cli/extract/delegate.js +9 -1
  12. package/dist/cli/extract/extraction.d.ts +4 -0
  13. package/dist/cli/extract/extraction.js +29 -2
  14. package/dist/cli/extract/harness.d.ts +12 -0
  15. package/dist/cli/extract/harness.js +3 -0
  16. package/dist/cli/extract/stages.js +5 -1
  17. package/dist/cli/init-scaffolds.d.ts +10 -1
  18. package/dist/cli/init-scaffolds.js +93 -1
  19. package/dist/cli/init.d.ts +3 -3
  20. package/dist/cli/init.js +53 -5
  21. package/dist/cli/playground/app/fake-client.js +11 -5
  22. package/dist/cli/playground/app/main.js +23 -13
  23. package/dist/cli/playground/app/scenario-mount.js +4 -1
  24. package/dist/cli/playground/app/theme-editor.js +27 -27
  25. package/dist/cli/playground/bundle.gen.d.ts +1 -1
  26. package/dist/cli/playground/bundle.gen.js +1 -1
  27. package/dist/cli/playground/embed-bundle.gen.d.ts +1 -1
  28. package/dist/cli/playground/embed-bundle.gen.js +1 -1
  29. package/dist/cli/shared.d.ts +1 -1
  30. package/dist/cli/shared.js +1 -1
  31. package/dist/cli/sync.d.ts +1 -1
  32. package/dist/cli/sync.js +1 -1
  33. package/dist/cloud-apps.js +2 -1
  34. package/dist/cloud-key-fetch.d.ts +16 -0
  35. package/dist/cloud-key-fetch.js +40 -0
  36. package/dist/cloud-tools.js +2 -1
  37. package/dist/connections.js +2 -2
  38. package/dist/dev-creds/model-edge.d.ts +21 -0
  39. package/dist/dev-creds/model-edge.js +24 -0
  40. package/dist/hosted-store.js +2 -1
  41. package/dist/refine.js +2 -1
  42. package/dist/sandbox.js +2 -2
  43. package/dist/server.d.ts +1 -1
  44. package/dist/server.js +56 -22
  45. package/dist/wire/shared.d.ts +2 -2
  46. package/dist/wire/shared.js +1 -1
  47. package/package.json +20 -11
@@ -1,6 +1,6 @@
1
1
  import { canonicalJson, sha256Hex, } from "@vendoai/core";
2
2
  import { envOptOut, loadConfig } from "@vendoai/telemetry";
3
- import { cloudFetch } from "./cli/cloud/client.js";
3
+ import { cloudKeyFetch } from "./cloud-key-fetch.js";
4
4
  const DEFAULT_DATA_DIR = ".vendo/data";
5
5
  const DEFAULT_BATCH_SIZE = 100;
6
6
  const DEFAULT_QUEUE_LIMIT = 1_000;
@@ -60,8 +60,7 @@ function createMissUploader(options) {
60
60
  unrefTimer(timeout);
61
61
  try {
62
62
  const surface = await options.surface;
63
- const response = await cloudFetch("/api/v1/misses", {
64
- auth: "key",
63
+ const response = await cloudKeyFetch("/api/v1/misses", {
65
64
  apiKey: options.apiKey,
66
65
  env: options.env,
67
66
  fetchImpl: options.fetchImpl,
@@ -5,11 +5,8 @@ export declare class CloudError extends Error {
5
5
  readonly status: number;
6
6
  constructor(code: string, message: string, status: number);
7
7
  }
8
- export interface CloudUrlOptions {
9
- apiUrl?: string;
10
- env?: Record<string, string | undefined>;
11
- }
12
- export declare function resolveCloudBaseUrl(options?: CloudUrlOptions): string;
8
+ export { resolveCloudBaseUrl, type CloudUrlOptions } from "../../cloud-key-fetch.js";
9
+ import { type CloudUrlOptions } from "../../cloud-key-fetch.js";
13
10
  export interface SessionStore {
14
11
  read(): Promise<CloudSession | null>;
15
12
  write(session: CloudSession): Promise<void>;
@@ -15,10 +15,10 @@ export class CloudError extends Error {
15
15
  this.status = status;
16
16
  }
17
17
  }
18
- export function resolveCloudBaseUrl(options = {}) {
19
- const value = options.apiUrl ?? (options.env ?? process.env).VENDO_CLOUD_URL ?? DEFAULT_CLOUD_URL;
20
- return value.replace(/\/+$/, "");
21
- }
18
+ // Base-URL resolution lives with the portable runtime helper so server-side
19
+ // code (capability misses) never imports this Node/CLI module.
20
+ export { resolveCloudBaseUrl } from "../../cloud-key-fetch.js";
21
+ import { resolveCloudBaseUrl } from "../../cloud-key-fetch.js";
22
22
  function requestUrl(path, options) {
23
23
  const base = resolveCloudBaseUrl(options);
24
24
  const suffix = base.endsWith("/api/v1") && path.startsWith("/api/v1/")
@@ -23,4 +23,4 @@ export declare function installedAiVersion(root: string): Promise<string | null>
23
23
  * Read the host project's dependency versions (deps + devDeps) for telemetry.
24
24
  * Non-throwing: a missing or malformed package.json returns {}.
25
25
  */
26
- export declare function detectDepVersions(root: string, framework: HostFramework): Promise<DepVersions>;
26
+ export declare function detectDepVersions(root: string, framework: HostFramework | "custom"): Promise<DepVersions>;
@@ -56,7 +56,7 @@ export async function detectDepVersions(root, framework) {
56
56
  const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
57
57
  const dependencies = { ...manifest.devDependencies, ...manifest.dependencies };
58
58
  const out = {};
59
- const frameworkPackage = framework === "unknown" ? undefined : FRAMEWORK_PACKAGE[framework];
59
+ const frameworkPackage = framework === "unknown" || framework === "custom" ? undefined : FRAMEWORK_PACKAGE[framework];
60
60
  const frameworkVersion = frameworkPackage === undefined ? undefined : bareVersion(dependencies[frameworkPackage]);
61
61
  if (frameworkVersion !== undefined)
62
62
  out.frameworkVersion = frameworkVersion;
@@ -18,6 +18,8 @@ export declare const DOCTOR_ERROR_CODES: {
18
18
  readonly "E-WIRE-004": "the Next.js root layout is not wrapped in <VendoRoot>";
19
19
  readonly "E-WIRE-005": "the @vendoai/vendo (or vendoai alias) dependency is not declared";
20
20
  readonly "E-WIRE-006": "no visible agent surface is mounted (<VendoRoot> alone renders nothing)";
21
+ readonly "E-WIRE-007": "no createVendo server wiring found in an unknown-framework host";
22
+ readonly "E-WIRE-008": "no <VendoRoot> found in an unknown-framework host's source";
21
23
  readonly "E-CFG-001": "a required .vendo/ config file is missing";
22
24
  readonly "E-CFG-002": ".vendo/data/.gitignore is missing";
23
25
  readonly "E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support";
@@ -30,6 +32,7 @@ export declare const DOCTOR_ERROR_CODES: {
30
32
  readonly "E-LIVE-004": "no execution venue is configured";
31
33
  readonly "E-LIVE-005": "the host /status does not report an execution venue (version skew)";
32
34
  readonly "E-LIVE-006": "the app's root page returns a server error while the wire answers";
35
+ readonly "E-LIVE-007": "the selected e2b execution venue is unusable (E2B_API_KEY or the e2b package is missing)";
33
36
  readonly "E-AUTH-001": "present credentials did not reach the host API";
34
37
  readonly "E-AUTH-002": "the present credential probe is unreachable";
35
38
  readonly "E-AUTH-003": "the present credential probe cannot run while the dev server is down";
@@ -49,6 +52,8 @@ export declare const DOCTOR_ERROR_CODES: {
49
52
  readonly "E-TURN-001": "the live model turn did not answer";
50
53
  readonly "E-TURN-002": "the live model turn cannot run while the dev server is down";
51
54
  readonly "E-CLOUD-001": "VENDO_API_KEY is set but not usable";
55
+ readonly "E-TOOLS-001": "every extracted host tool is disabled or excluded (zero live host tools)";
56
+ readonly "E-TOOLS-002": "the extracted tool surface is empty (zero host tools)";
52
57
  };
53
58
  export type DoctorErrorCode = keyof typeof DOCTOR_ERROR_CODES;
54
59
  /** Complete list of every code doctor can emit, for CI enumeration. */
@@ -19,6 +19,8 @@ export const DOCTOR_ERROR_CODES = {
19
19
  "E-WIRE-004": "the Next.js root layout is not wrapped in <VendoRoot>",
20
20
  "E-WIRE-005": "the @vendoai/vendo (or vendoai alias) dependency is not declared",
21
21
  "E-WIRE-006": "no visible agent surface is mounted (<VendoRoot> alone renders nothing)",
22
+ "E-WIRE-007": "no createVendo server wiring found in an unknown-framework host",
23
+ "E-WIRE-008": "no <VendoRoot> found in an unknown-framework host's source",
22
24
  "E-CFG-001": "a required .vendo/ config file is missing",
23
25
  "E-CFG-002": ".vendo/data/.gitignore is missing",
24
26
  "E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support",
@@ -31,6 +33,7 @@ export const DOCTOR_ERROR_CODES = {
31
33
  "E-LIVE-004": "no execution venue is configured",
32
34
  "E-LIVE-005": "the host /status does not report an execution venue (version skew)",
33
35
  "E-LIVE-006": "the app's root page returns a server error while the wire answers",
36
+ "E-LIVE-007": "the selected e2b execution venue is unusable (E2B_API_KEY or the e2b package is missing)",
34
37
  "E-AUTH-001": "present credentials did not reach the host API",
35
38
  "E-AUTH-002": "the present credential probe is unreachable",
36
39
  "E-AUTH-003": "the present credential probe cannot run while the dev server is down",
@@ -50,6 +53,8 @@ export const DOCTOR_ERROR_CODES = {
50
53
  "E-TURN-001": "the live model turn did not answer",
51
54
  "E-TURN-002": "the live model turn cannot run while the dev server is down",
52
55
  "E-CLOUD-001": "VENDO_API_KEY is set but not usable",
56
+ "E-TOOLS-001": "every extracted host tool is disabled or excluded (zero live host tools)",
57
+ "E-TOOLS-002": "the extracted tool surface is empty (zero host tools)",
53
58
  };
54
59
  /** Complete list of every code doctor can emit, for CI enumeration. */
55
60
  export const doctorErrorCodes = Object.keys(DOCTOR_ERROR_CODES);
@@ -34,6 +34,7 @@ export interface DoctorOptions {
34
34
  ok: boolean;
35
35
  stop: () => void;
36
36
  }>;
37
+ e2bResolvable?: (root: string) => boolean;
37
38
  }
38
39
  /** Doctor runs standalone, so unlike the dev server it gets no framework
39
40
  * dotenv loading — without this, `VENDO_API_KEY` sitting in `.env.local`
@@ -1,14 +1,30 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import { join, resolve } from "node:path";
3
4
  import { stdin, stdout } from "node:process";
4
5
  import { cloudDoctor, liveModelTurn, startDevServerForProbe, } from "./doctor-live.js";
5
6
  import { installedAiVersion } from "./dep-versions.js";
6
7
  import { doctorFixRef } from "./doctor-codes.js";
7
8
  import { EJECT_MANIFEST_FILE } from "./eject.js";
9
+ import { overridesFileSchema, toolsFileSchema } from "@vendoai/actions";
8
10
  import { detectFramework, detectVendoWiring } from "./framework.js";
9
11
  import { walk } from "./theme/walk.js";
10
12
  import { remoteUrls, sameUrl, validateRegistryServer } from "./mcp/registry.js";
11
13
  import { askYesNo, CLI_VERSION, consoleOutput, exists, normalizeDotEnvValue, readOptional, toolingTelemetry } from "./shared.js";
14
+ /** Whether the optional `e2b` SDK resolves from the target project — the same
15
+ * node_modules walk the running wire's dynamic `import("e2b")` performs, so
16
+ * doctor certifies the venue against the resolution that will actually be
17
+ * asked to load it (0.4.4 defect C: /status said e2b on a host without the
18
+ * SDK, and the first build died in an unusable venue). */
19
+ function e2bResolvableFrom(root) {
20
+ try {
21
+ createRequire(join(root, "__vendo-doctor-probe__.js")).resolve("e2b");
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
12
28
  async function hasDependency(root) {
13
29
  try {
14
30
  const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
@@ -112,7 +128,21 @@ export async function runDoctor(options) {
112
128
  }
113
129
  const scanned = await detectVendoWiring(root, { exclude: wrapperCandidates });
114
130
  const wiring = { ...scanned, surface: scanned.surface || (scanned.client && wrapperWithOverlay) };
115
- if (framework === "express") {
131
+ if (framework === "unknown") {
132
+ // No framework to pattern-match (field case: a Cloudflare Worker + Vite
133
+ // host failed E-WIRE-003/004 forever) — judge the wiring by the same
134
+ // bounded source scan init uses, never by another framework's file
135
+ // layout. The surface check below still runs; it is source-generic.
136
+ if (scanned.server)
137
+ pass("wiring/server", "createVendo server wiring found");
138
+ else
139
+ fail("wiring/server", "E-WIRE-007", "no createVendo server wiring found — import createVendo from @vendoai/vendo/server and mount vendo.handler on your runtime's request entry");
140
+ if (scanned.client)
141
+ pass("wiring/client", "<VendoRoot> wraps the client");
142
+ else
143
+ warn("wiring/client", "E-WIRE-008", "no <VendoRoot> found in the host source — the @vendoai/ui hooks and embeds need it; ignore this if the host renders a fully custom surface");
144
+ }
145
+ else if (framework === "express") {
116
146
  if (wiring.server)
117
147
  pass("wiring/express-server", "Express server is wired");
118
148
  else
@@ -184,6 +214,42 @@ export async function runDoctor(options) {
184
214
  }
185
215
  if (!await exists(join(root, ".vendo", "data", ".gitignore")))
186
216
  warn("config/data-gitignore", "E-CFG-002", ".vendo/data/.gitignore is missing");
217
+ // The core promise, statically checkable: does the agent have any HOST
218
+ // tool it may actually call? All-disabled is an explicit misconfiguration
219
+ // (fail); an empty extraction is a strong warning — connector-only hosts
220
+ // are legitimate, but a fresh install landing here means extraction found
221
+ // nothing user-facing (field case: an infra product whose surface was all
222
+ // internal endpoints ended with tools: [] and a silently useless agent).
223
+ const toolsRaw = await readOptional(join(root, ".vendo", "tools.json"));
224
+ const overridesRaw = await readOptional(join(root, ".vendo", "overrides.json"));
225
+ if (toolsRaw !== null) {
226
+ try {
227
+ const toolsFile = toolsFileSchema.parse(JSON.parse(toolsRaw));
228
+ let overridesTools = {};
229
+ if (overridesRaw !== null) {
230
+ try {
231
+ overridesTools = overridesFileSchema.parse(JSON.parse(overridesRaw)).tools;
232
+ }
233
+ catch {
234
+ // Malformed overrides are their own (pre-existing) failure surface.
235
+ }
236
+ }
237
+ const live = toolsFile.tools.filter((tool) => (overridesTools[tool.name]?.disabled ?? tool.disabled ?? false) !== true);
238
+ if (toolsFile.tools.length === 0) {
239
+ warn("tools/live-surface", "E-TOOLS-002", "the extracted tool surface is empty — the agent cannot act on this product's API; re-run `vendo init` extraction (or ignore if this deployment is connector-only)");
240
+ }
241
+ else if (live.length === 0) {
242
+ fail("tools/live-surface", "E-TOOLS-001", `zero live host tools — all ${toolsFile.tools.length} extracted tools are disabled or excluded; review the audience exclusions in .vendo/overrides.json and re-enable the end-user surface (disabled: false)`);
243
+ }
244
+ else {
245
+ pass("tools/live-surface", `${live.length} live host tool${live.length === 1 ? "" : "s"}`);
246
+ }
247
+ }
248
+ catch {
249
+ // Not the vendo/tools@1 shape (e.g. a placeholder {}) — the config
250
+ // checks above already govern presence; nothing to grade here.
251
+ }
252
+ }
187
253
  // §4 customization ladder — ejected chrome drift. The ejected pixels are the
188
254
  // host's code, so a version gap is awareness (warn), never breakage (fail):
189
255
  // the hooks/wire dependency keeps working; only new presentation is missed.
@@ -279,7 +345,25 @@ export async function runDoctor(options) {
279
345
  // 10-mcp §1 — the door flag lives under blocks.mcp.
280
346
  mcpEnabled = body.blocks.mcp === true;
281
347
  sandboxVenue = body.blocks.sandbox;
282
- if (sandboxVenue === "e2b" || sandboxVenue === "cloud" || sandboxVenue === "custom") {
348
+ if (sandboxVenue === "e2b") {
349
+ // 0.4.4 defect C — "ok: execution venue: e2b" on a host that cannot
350
+ // actually run e2b is a false blessing: the venue must be USABLE
351
+ // (key set and SDK resolvable from this project), or every server-app
352
+ // build dies in it instead of riding the Cloud sandbox.
353
+ const keyPresent = typeof env.E2B_API_KEY === "string" && env.E2B_API_KEY.trim() !== "";
354
+ const installed = (options.e2bResolvable ?? e2bResolvableFrom)(root);
355
+ if (keyPresent && installed) {
356
+ pass("live/venue", "execution venue: e2b");
357
+ }
358
+ else {
359
+ const missing = [
360
+ ...(keyPresent ? [] : ["E2B_API_KEY is not set"]),
361
+ ...(installed ? [] : ["the e2b package does not resolve from this project"]),
362
+ ].join(" and ");
363
+ fail("live/venue", "E-LIVE-007", `the running wire selected the e2b execution venue but ${missing}; server-app builds will fail in an unusable sandbox. Fix: install the e2b package and set E2B_API_KEY, or remove E2B_API_KEY from the server env (with VENDO_API_KEY set, the managed Cloud sandbox takes over), then restart the dev server and re-run doctor`);
364
+ }
365
+ }
366
+ else if (sandboxVenue === "cloud" || sandboxVenue === "custom") {
283
367
  pass("live/venue", `execution venue: ${sandboxVenue}`);
284
368
  }
285
369
  else if (sandboxVenue === false) {
@@ -1,6 +1,6 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import { z } from "zod";
3
- import { vendoSync } from "@vendoai/actions";
3
+ import { vendoSync } from "@vendoai/actions/sync";
4
4
  import { detectDepVersions } from "../dep-versions.js";
5
5
  import { detectFramework } from "../framework.js";
6
6
  import { applyDraft, reportApplied } from "./extraction.js";
@@ -41,6 +41,10 @@ export const EXTRACTION_DRAFT_JSON_SCHEMA = {
41
41
  type: "boolean",
42
42
  description: "false = wake a statically-unclassifiable tool (requires reasoning and risk).",
43
43
  },
44
+ audience: {
45
+ enum: ["end-user", "operator", "internal"],
46
+ description: "Who the handler's own auth admits; non-end-user grades exclude the tool by default.",
47
+ },
44
48
  reasoning: { type: "string", maxLength: 500 },
45
49
  },
46
50
  },
@@ -66,10 +70,14 @@ export function composeDelegatedInstructions(tools, appName) {
66
70
  "",
67
71
  "Rules:",
68
72
  "- Produce ONE json document matching the provided draftSchema:",
69
- ' { "brief": string, "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "reasoning"? }], "missedSurfaces"?: string[] }',
73
+ ' { "brief": string, "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "audience"?, "reasoning"? }], "missedSurfaces"?: string[] }',
70
74
  "- tools: include ONLY names from the list above. Rewrite each description so an agent choosing tools understands what it actually does (read the handler source). <= 200 chars each.",
71
75
  "- risk: you may RAISE risk (read->write->destructive) when the handler is more dangerous than labeled; never lower it. Mark irreversible operations critical: true.",
72
76
  "- A tool listed as disabled was statically unclassifiable. If you can read its handler and grade it, set disabled: false WITH a risk and one-line reasoning. Leave it out otherwise.",
77
+ "- audience: who the handler's own auth admits — \"end-user\" (a signed-in customer acting on their own data),",
78
+ " \"operator\" (admin/staff/support consoles), or \"internal\" (machine-to-machine: webhooks, cron, reconciliation,",
79
+ " service tokens). Read the auth checks, not the route name. When unsure, default to internal — non-end-user",
80
+ " tools are excluded from the embedded agent by default, and a wrong \"end-user\" grade exposes a privileged surface.",
73
81
  "- missedSurfaces: API surfaces you found that the list is missing (path + one line). Do not invent tools for them.",
74
82
  "- brief: one paragraph — what the product does, who uses it, the jobs the agent should help with. Written from the actual code, no marketing fluff.",
75
83
  `- Save the draft to a json file and apply it with \`${APPLY_COMMAND}\` — deterministic guards validate every entry before anything lands in .vendo/.`,
@@ -8,6 +8,10 @@ export interface AppliedSummary {
8
8
  riskRaised: number;
9
9
  critical: number;
10
10
  woken: number;
11
+ /** Non-end-user-audience tools excluded (disabled) this apply. */
12
+ excluded: number;
13
+ /** Tools still live after this apply — zero means the agent cannot act on the host API at all. */
14
+ enabledAfter: number;
11
15
  briefWritten: boolean;
12
16
  refused: string[];
13
17
  missedSurfaces: string[];
@@ -50,7 +50,7 @@ export async function applyDraft(input) {
50
50
  ? { format: "vendo/overrides@1", tools: {} }
51
51
  : overridesSchema.parse(JSON.parse(raw));
52
52
  const summary = {
53
- described: 0, riskRaised: 0, critical: 0, woken: 0,
53
+ described: 0, riskRaised: 0, critical: 0, woken: 0, excluded: 0, enabledAfter: 0,
54
54
  briefWritten: false, refused: [], missedSurfaces: input.draft.missedSurfaces ?? [],
55
55
  };
56
56
  for (const entry of input.draft.tools) {
@@ -70,7 +70,10 @@ export async function applyDraft(input) {
70
70
  // evidence, so a reasoned wake carries the model's grade without tripping
71
71
  // the downgrade guard (Greptile P1 / Devin on #363). A human-set risk or
72
72
  // disabled decision always wins.
73
- const isWake = entry.disabled === false && fact.disabled === true;
73
+ // A wake claiming a non-end-user audience is contradictory; audience
74
+ // exclusion (fail-closed) wins and the wake leg never runs.
75
+ const nonEndUser = entry.audience === "operator" || entry.audience === "internal";
76
+ const isWake = entry.disabled === false && fact.disabled === true && !nonEndUser;
74
77
  if (isWake && existing.disabled === undefined) {
75
78
  if (entry.reasoning === undefined || entry.risk === undefined) {
76
79
  summary.refused.push(`${entry.name}: waking a disabled tool needs reasoning and a risk grade`);
@@ -96,9 +99,27 @@ export async function applyDraft(input) {
96
99
  next.critical = true;
97
100
  summary.critical += 1;
98
101
  }
102
+ // Audience: recorded as provenance; a non-end-user grade excludes the
103
+ // tool by default. A human decision on either field always wins — a
104
+ // hand-set disabled (true OR false) or audience is never touched.
105
+ if (entry.audience !== undefined && existing.audience === undefined) {
106
+ next.audience = entry.audience;
107
+ if (nonEndUser && existing.disabled === undefined && next.disabled !== false) {
108
+ if (next.disabled !== true)
109
+ summary.excluded += 1;
110
+ next.disabled = true;
111
+ }
112
+ }
99
113
  if (JSON.stringify(next) !== JSON.stringify(existing))
100
114
  overrides.tools[entry.name] = next;
101
115
  }
116
+ // The number that actually matters to an end user: how many tools the
117
+ // agent can still call once static grades and overrides settle. Zero means
118
+ // the product's core promise silently degraded — reportApplied warns.
119
+ summary.enabledAfter = input.tools.filter((tool) => {
120
+ const override = overrides.tools[tool.name] ?? {};
121
+ return (override.disabled ?? tool.disabled ?? false) !== true;
122
+ }).length;
102
123
  await writeText(overridesPath, `${JSON.stringify(overrides, null, 2)}\n`);
103
124
  const briefPath = join(input.root, ".vendo", "brief.md");
104
125
  const currentBrief = ((await readOptional(briefPath)) ?? "").trim();
@@ -117,6 +138,7 @@ export function reportApplied(input) {
117
138
  ...(applied.riskRaised > 0 ? [`${applied.riskRaised} risk raises`] : []),
118
139
  ...(applied.critical > 0 ? [`${applied.critical} critical marks`] : []),
119
140
  ...(applied.woken > 0 ? [`${applied.woken} tools woken`] : []),
141
+ ...(applied.excluded > 0 ? [`${applied.excluded} operator/internal tools excluded`] : []),
120
142
  ...(input.briefDrafted ? ["brief drafted"] : []),
121
143
  ];
122
144
  output.log(`AI polish applied: ${parts.join(" · ")} → .vendo/overrides.json, .vendo/brief.md${input.artifactsNote ?? ""}`);
@@ -124,6 +146,11 @@ export function reportApplied(input) {
124
146
  output.error(` ${note}`);
125
147
  for (const refused of applied.refused)
126
148
  output.error(` refused: ${refused}`);
149
+ if (applied.enabledAfter === 0) {
150
+ output.error(" WARNING: zero live tools after apply — every extracted tool is disabled or excluded, so the embedded "
151
+ + "agent cannot act on this product's API. Review the audience exclusions in .vendo/overrides.json "
152
+ + "(re-enable the end-user surface with disabled: false) or re-run extraction.");
153
+ }
127
154
  for (const missed of applied.missedSurfaces)
128
155
  output.log(` missed surface (not extracted yet): ${missed}`);
129
156
  }
@@ -15,10 +15,14 @@ export declare const draftToolSchema: z.ZodObject<{
15
15
  critical: z.ZodOptional<z.ZodBoolean>;
16
16
  /** false = wake a statically-unclassifiable tool (needs reasoning). */
17
17
  disabled: z.ZodOptional<z.ZodBoolean>;
18
+ /** Who the handler's own auth admits; non-end-user grades exclude the tool
19
+ * by default (applyDraft). */
20
+ audience: z.ZodOptional<z.ZodEnum<["end-user", "operator", "internal"]>>;
18
21
  reasoning: z.ZodOptional<z.ZodString>;
19
22
  }, "strip", z.ZodTypeAny, {
20
23
  name: string;
21
24
  description: string;
25
+ audience?: "end-user" | "operator" | "internal" | undefined;
22
26
  risk?: "read" | "write" | "destructive" | undefined;
23
27
  critical?: boolean | undefined;
24
28
  disabled?: boolean | undefined;
@@ -26,6 +30,7 @@ export declare const draftToolSchema: z.ZodObject<{
26
30
  }, {
27
31
  name: string;
28
32
  description: string;
33
+ audience?: "end-user" | "operator" | "internal" | undefined;
29
34
  risk?: "read" | "write" | "destructive" | undefined;
30
35
  critical?: boolean | undefined;
31
36
  disabled?: boolean | undefined;
@@ -42,10 +47,14 @@ export declare const extractionDraftSchema: z.ZodObject<{
42
47
  critical: z.ZodOptional<z.ZodBoolean>;
43
48
  /** false = wake a statically-unclassifiable tool (needs reasoning). */
44
49
  disabled: z.ZodOptional<z.ZodBoolean>;
50
+ /** Who the handler's own auth admits; non-end-user grades exclude the tool
51
+ * by default (applyDraft). */
52
+ audience: z.ZodOptional<z.ZodEnum<["end-user", "operator", "internal"]>>;
45
53
  reasoning: z.ZodOptional<z.ZodString>;
46
54
  }, "strip", z.ZodTypeAny, {
47
55
  name: string;
48
56
  description: string;
57
+ audience?: "end-user" | "operator" | "internal" | undefined;
49
58
  risk?: "read" | "write" | "destructive" | undefined;
50
59
  critical?: boolean | undefined;
51
60
  disabled?: boolean | undefined;
@@ -53,6 +62,7 @@ export declare const extractionDraftSchema: z.ZodObject<{
53
62
  }, {
54
63
  name: string;
55
64
  description: string;
65
+ audience?: "end-user" | "operator" | "internal" | undefined;
56
66
  risk?: "read" | "write" | "destructive" | undefined;
57
67
  critical?: boolean | undefined;
58
68
  disabled?: boolean | undefined;
@@ -65,6 +75,7 @@ export declare const extractionDraftSchema: z.ZodObject<{
65
75
  tools: {
66
76
  name: string;
67
77
  description: string;
78
+ audience?: "end-user" | "operator" | "internal" | undefined;
68
79
  risk?: "read" | "write" | "destructive" | undefined;
69
80
  critical?: boolean | undefined;
70
81
  disabled?: boolean | undefined;
@@ -76,6 +87,7 @@ export declare const extractionDraftSchema: z.ZodObject<{
76
87
  tools: {
77
88
  name: string;
78
89
  description: string;
90
+ audience?: "end-user" | "operator" | "internal" | undefined;
79
91
  risk?: "read" | "write" | "destructive" | undefined;
80
92
  critical?: boolean | undefined;
81
93
  disabled?: boolean | undefined;
@@ -15,6 +15,9 @@ export const draftToolSchema = z.object({
15
15
  critical: z.boolean().optional(),
16
16
  /** false = wake a statically-unclassifiable tool (needs reasoning). */
17
17
  disabled: z.boolean().optional(),
18
+ /** Who the handler's own auth admits; non-end-user grades exclude the tool
19
+ * by default (applyDraft). */
20
+ audience: z.enum(["end-user", "operator", "internal"]).optional(),
18
21
  reasoning: z.string().max(500).optional(),
19
22
  });
20
23
  export const extractionDraftSchema = z.object({
@@ -115,10 +115,14 @@ export function composeInstructions(tools, appName, surface) {
115
115
  "",
116
116
  "Rules:",
117
117
  "- Reply with ONLY one fenced json block matching:",
118
- ' { "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "reasoning"? }], "missedSurfaces"?: string[] }',
118
+ ' { "tools": [{ "name", "description", "risk"?, "critical"?, "disabled"?, "audience"?, "reasoning"? }], "missedSurfaces"?: string[] }',
119
119
  "- tools: include ONLY names from the list above. Rewrite each description so an agent choosing tools understands what it actually does (read the handler source). <= 200 chars each.",
120
120
  "- risk: you may RAISE risk (read->write->destructive) when the handler is more dangerous than labeled; never lower it. Mark irreversible operations critical: true.",
121
121
  "- A tool listed as disabled was statically unclassifiable. If you can read its handler and grade it, set disabled: false WITH a risk and one-line reasoning. Leave it out otherwise.",
122
+ "- audience: who the handler's own auth admits — \"end-user\" (a signed-in customer acting on their own data),",
123
+ " \"operator\" (admin/staff/support consoles), or \"internal\" (machine-to-machine: webhooks, cron, reconciliation,",
124
+ " service tokens). Read the auth checks, not the route name. When unsure, default to internal — non-end-user",
125
+ " tools are excluded from the embedded agent by default, and a wrong \"end-user\" grade exposes a privileged surface.",
122
126
  "- missedSurfaces: API surfaces you found that the list is missing (path + one line). Do not invent tools for them.",
123
127
  ].join("\n");
124
128
  }
@@ -1,4 +1,4 @@
1
- import { type ServerActionRegistration } from "@vendoai/actions";
1
+ import { type ServerActionRegistration } from "@vendoai/actions/sync";
2
2
  import { type AuthMatch } from "./init-auth.js";
3
3
  /** The wired preset line plus its escape-hatch comment. The lead-in stays
4
4
  honest about how the preset got here: detection cites the found
@@ -46,5 +46,14 @@ export declare function wiringServerActions(root: string): Promise<ServerActionR
46
46
  * sorted registrations, stable aliases — so re-init stays idempotent.
47
47
  */
48
48
  export declare function serverActionsModuleSource(root: string, wiringDir: string, registrations: ServerActionRegistration[]): string;
49
+ /** The runtime-neutral composition (`--framework custom`): plain Request →
50
+ * Response with env passed per call, so ONE generated module serves any
51
+ * Web-standard host — Cloudflare Workers, Bun, Deno, Hono, Lambda adapters.
52
+ * Construction is lazy (first request): the safe shape everywhere and the
53
+ * only legal one at Workers module scope. With a Vendo Cloud key the four
54
+ * infrastructure seams wire the Cloud adapters explicitly per the adapter
55
+ * rule (reference shape: the vendo-on-Workers field integration,
56
+ * 2026-07-21). */
57
+ export declare function customServerSource(typescript: boolean, auth?: AuthMatch | null): string;
49
58
  export declare function expressServerSource(typescript: boolean, auth?: AuthMatch | null): string;
50
59
  export declare const VENDO_ENV_EXAMPLE: string;
@@ -1,5 +1,5 @@
1
1
  import { join, relative, sep } from "node:path";
2
- import { extractServerActions, serverActionRegistrations, } from "@vendoai/actions";
2
+ import { extractServerActions, serverActionRegistrations, } from "@vendoai/actions/sync";
3
3
  import { AUTH_FAMILY_INFO, AUTH_PRESET_SPECIFIER } from "./init-auth.js";
4
4
  /** The wired preset line plus its escape-hatch comment. The lead-in stays
5
5
  honest about how the preset got here: detection cites the found
@@ -161,6 +161,98 @@ export function serverActionsModuleSource(root, wiringDir, registrations) {
161
161
  return `${header}${imports.join("\n")}\n\n` +
162
162
  `export const serverActions = {\n${entries.join("\n")}\n};\n`;
163
163
  }
164
+ /** The runtime-neutral composition (`--framework custom`): plain Request →
165
+ * Response with env passed per call, so ONE generated module serves any
166
+ * Web-standard host — Cloudflare Workers, Bun, Deno, Hono, Lambda adapters.
167
+ * Construction is lazy (first request): the safe shape everywhere and the
168
+ * only legal one at Workers module scope. With a Vendo Cloud key the four
169
+ * infrastructure seams wire the Cloud adapters explicitly per the adapter
170
+ * rule (reference shape: the vendo-on-Workers field integration,
171
+ * 2026-07-21). */
172
+ export function customServerSource(typescript, auth = null) {
173
+ const registrySpecifier = typescript ? "./registry" : "./registry.mjs";
174
+ const envType = typescript
175
+ ? `\nexport interface VendoEnv {\n` +
176
+ ` VENDO_API_KEY?: string;\n` +
177
+ ` VENDO_CLOUD_URL?: string;\n` +
178
+ ` VENDO_BASE_URL?: string;\n` +
179
+ `}\n`
180
+ : "";
181
+ const signatures = typescript
182
+ ? {
183
+ vendoVar: `let vendo: ReturnType<typeof createVendo> | null = null;`,
184
+ getVendo: `(env: VendoEnv = {})`,
185
+ handle: `(request: Request, env: VendoEnv = {}): Promise<Response>`,
186
+ }
187
+ : {
188
+ vendoVar: `let vendo = null;`,
189
+ getVendo: `(env = {})`,
190
+ handle: `(request, env = {})`,
191
+ };
192
+ const clientHint = typescript
193
+ ? ` * // in the client entry — theme.json adopts the host brand (08 §4);\n` +
194
+ ` * // the cast narrows TypeScript's widened JSON-module string literals;\n` +
195
+ ` * // <VendoOverlay /> is the visible surface (launcher pill + panel):\n` +
196
+ ` * import { VendoOverlay, VendoRoot } from "@vendoai/vendo/react";\n` +
197
+ ` * import { registry } from "<path-to>/vendo/registry";\n` +
198
+ ` * import theme from "<path-to>/.vendo/theme.json";\n` +
199
+ ` * import type { VendoTheme } from "@vendoai/vendo";\n` +
200
+ ` * root.render(<VendoRoot components={registry} theme={theme as VendoTheme}><App /><VendoOverlay /></VendoRoot>);\n`
201
+ : ` * // in the client entry — theme.json adopts the host brand (08 §4);\n` +
202
+ ` * // <VendoOverlay /> is the visible surface (launcher pill + panel):\n` +
203
+ ` * import { VendoOverlay, VendoRoot } from "@vendoai/vendo/react";\n` +
204
+ ` * import { registry } from "<path-to>/vendo/registry.mjs";\n` +
205
+ ` * import theme from "<path-to>/.vendo/theme.json";\n` +
206
+ ` * root.render(<VendoRoot components={registry} theme={theme}><App /><VendoOverlay /></VendoRoot>);\n`;
207
+ return `/**\n` +
208
+ ` * Route your runtime's requests through this module:\n` +
209
+ ` * // Cloudflare Workers:\n` +
210
+ ` * // export default { fetch: (request, env) => handleVendoRequest(request, env) };\n` +
211
+ ` * // Bun / Deno / Hono / Node: serve your /api/vendo routes through\n` +
212
+ ` * // handleVendoRequest(request)\n` +
213
+ clientHint +
214
+ ` * Deployed hosts must set VENDO_BASE_URL to their public origin\n` +
215
+ ` * (credential forwarding fails closed without it — vendo doctor checks).\n` +
216
+ ` */\n` +
217
+ `import { createAnthropic } from "@ai-sdk/anthropic";\n` +
218
+ authImportLine(auth) +
219
+ `import { cloudConnections, cloudSandbox, cloudTools, createVendo, hostedStore } from "@vendoai/vendo/server";\n` +
220
+ `import { registry } from ${JSON.stringify(registrySpecifier)};\n` +
221
+ envType +
222
+ `\n${signatures.vendoVar}\n` +
223
+ `\n/** Lazy singleton: constructed on the first request, never at module\n` +
224
+ ` scope — Workers forbids I/O and timers there, and lazy is correct on\n` +
225
+ ` every other runtime too. */\n` +
226
+ `function getVendo${signatures.getVendo} {\n` +
227
+ ` if (vendo === null) {\n` +
228
+ ` const processEnv = globalThis.process?.env ?? {};\n` +
229
+ ` const apiKey = env.VENDO_API_KEY ?? processEnv.VENDO_API_KEY;\n` +
230
+ ` const baseUrl = (env.VENDO_CLOUD_URL ?? processEnv.VENDO_CLOUD_URL ?? "https://console.vendo.run").replace(/\\/+$/, "");\n` +
231
+ ` const cloud = apiKey === undefined || apiKey === "" ? undefined : { apiKey, baseUrl };\n` +
232
+ ` vendo = createVendo({\n` +
233
+ (auth === null ? anonymousPrincipalLines() : authConfigLines(auth))
234
+ .split("\n").map((line) => (line === "" ? line : ` ${line}`)).join("\n") +
235
+ ` catalog: registry,\n` +
236
+ ` policy: {}, // .vendo/policy.json: destructive asks, reads run\n` +
237
+ ` // With a Vendo Cloud key the infrastructure seams wire the Cloud\n` +
238
+ ` // adapters EXPLICITLY (composition decides; blocks never read the\n` +
239
+ ` // environment). Without one, pass your own adapters here — model,\n` +
240
+ ` // store, connections, sandbox all accept custom implementations.\n` +
241
+ ` ...(cloud === undefined ? {} : {\n` +
242
+ ` model: createAnthropic({ apiKey: cloud.apiKey, baseURL: \`\${cloud.baseUrl}/api/v1\` })("vendo-default"),\n` +
243
+ ` store: hostedStore(cloud),\n` +
244
+ ` connections: cloudConnections(cloud),\n` +
245
+ ` connectors: [cloudTools(cloud)],\n` +
246
+ ` sandbox: cloudSandbox(cloud),\n` +
247
+ ` }),\n` +
248
+ ` });\n` +
249
+ ` }\n` +
250
+ ` return vendo;\n` +
251
+ `}\n` +
252
+ `\nexport function handleVendoRequest${signatures.handle} {\n` +
253
+ ` return getVendo(env).handler(request);\n` +
254
+ `}\n`;
255
+ }
164
256
  export function expressServerSource(typescript, auth = null) {
165
257
  const imports = typescript
166
258
  ? `import { once } from "node:events";\n` +
@@ -1,4 +1,4 @@
1
- import { type ExtractedTool } from "@vendoai/actions";
1
+ import type { ExtractedTool } from "@vendoai/actions";
2
2
  import { type CloudStepOptions } from "./cloud-init.js";
3
3
  import { type AiExtractionOptions } from "./extract/extraction.js";
4
4
  import { type DevCredential } from "../dev-creds/resolve.js";
@@ -14,7 +14,7 @@ export interface RiskRecommendation {
14
14
  recommendation: string;
15
15
  }
16
16
  export interface InitPlan {
17
- framework: HostFramework;
17
+ framework: Exclude<HostFramework, "unknown"> | "custom";
18
18
  root: string;
19
19
  writes: string[];
20
20
  codeChanges: Array<{
@@ -54,7 +54,7 @@ export interface InitOptions {
54
54
  detection comes back "unknown" (there is no safe default to guess).
55
55
  "unknown" is excluded: an override that answers nothing would silently
56
56
  bypass the non-interactive framework guard. */
57
- framework?: Exclude<HostFramework, "unknown">;
57
+ framework?: Exclude<HostFramework, "unknown"> | "custom";
58
58
  /** --cloud-key: answer the cloud-login offer with an existing key — landed
59
59
  in .env.local exactly where the mint would put it. */
60
60
  cloudKey?: string;