@vendoai/vendo 0.4.2 → 0.4.4
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/capability-misses.js +2 -3
- package/dist/cli/cloud/client.d.ts +2 -5
- package/dist/cli/cloud/client.js +4 -4
- package/dist/cli/cloud/device-login.d.ts +11 -0
- package/dist/cli/cloud/device-login.js +52 -1
- package/dist/cli/dep-versions.d.ts +1 -1
- package/dist/cli/dep-versions.js +1 -1
- package/dist/cli/doctor-codes.d.ts +7 -0
- package/dist/cli/doctor-codes.js +7 -0
- package/dist/cli/doctor-live.js +11 -1
- package/dist/cli/doctor.js +122 -11
- package/dist/cli/extract/apply.js +1 -1
- package/dist/cli/extract/delegate.js +9 -1
- package/dist/cli/extract/extraction.d.ts +4 -0
- package/dist/cli/extract/extraction.js +29 -2
- package/dist/cli/extract/harness.d.ts +12 -0
- package/dist/cli/extract/harness.js +3 -0
- package/dist/cli/extract/stages.js +5 -1
- package/dist/cli/framework.d.ts +17 -2
- package/dist/cli/framework.js +32 -5
- package/dist/cli/init-scaffolds.d.ts +32 -1
- package/dist/cli/init-scaffolds.js +153 -9
- package/dist/cli/init.d.ts +14 -4
- package/dist/cli/init.js +212 -43
- package/dist/cli/playground/app/fake-client.js +11 -5
- package/dist/cli/playground/app/main.js +23 -13
- package/dist/cli/playground/app/scenario-mount.js +4 -1
- package/dist/cli/playground/app/theme-editor.js +27 -27
- package/dist/cli/playground/bundle.gen.d.ts +1 -1
- package/dist/cli/playground/bundle.gen.js +1 -1
- package/dist/cli/playground/embed-bundle.gen.d.ts +1 -1
- package/dist/cli/playground/embed-bundle.gen.js +1 -1
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +1 -1
- package/dist/cli/sync.d.ts +1 -1
- package/dist/cli/sync.js +1 -1
- package/dist/cloud-apps.js +2 -1
- package/dist/cloud-key-fetch.d.ts +16 -0
- package/dist/cloud-key-fetch.js +40 -0
- package/dist/cloud-tools.js +2 -1
- package/dist/connections.js +2 -2
- package/dist/dev-creds/model-edge.d.ts +21 -0
- package/dist/dev-creds/model-edge.js +24 -0
- package/dist/hosted-store.js +2 -1
- package/dist/mastra.js +71 -2
- package/dist/react.d.ts +1 -0
- package/dist/react.js +7 -0
- package/dist/refine.js +2 -1
- package/dist/sandbox.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +56 -22
- package/dist/wire/apps.js +30 -1
- package/dist/wire/shared.d.ts +2 -2
- package/dist/wire/shared.js +1 -1
- 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 {
|
|
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
|
|
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
|
|
9
|
-
|
|
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>;
|
package/dist/cli/cloud/client.js
CHANGED
|
@@ -15,10 +15,10 @@ export class CloudError extends Error {
|
|
|
15
15
|
this.status = status;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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/")
|
|
@@ -19,6 +19,17 @@ export interface DeviceLoginOptions {
|
|
|
19
19
|
file that lets a fresh run resume a still-open ceremony (#479). */
|
|
20
20
|
home?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Write-preflight (0.4.1 E2E cert M4): prove `.env.local` is writable BEFORE
|
|
24
|
+
* any claim is opened or redeemed. Sandboxed agent runs — headless Claude
|
|
25
|
+
* Code protects env files even under --dangerously-skip-permissions — can
|
|
26
|
+
* deny the write; without this check the ceremony redeems the single-use
|
|
27
|
+
* claim, the key mints server-side, and the write failure loses it. A real
|
|
28
|
+
* append-mode open is the probe (permission checks like `access(W_OK)` don't
|
|
29
|
+
* see sandbox policies, which deny at open time); a probe-created empty file
|
|
30
|
+
* is removed again. Returns the failure detail, or null when writable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function preflightEnvLocalWrite(root: string): Promise<string | null>;
|
|
22
33
|
/**
|
|
23
34
|
* `vendo login` — the top-level command surface: the identical ceremony
|
|
24
35
|
* wrapped in one `command_run` row (command "login", TELEMETRY.md). The
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { open, stat, unlink } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { option } from "./args.js";
|
|
4
5
|
import { isVendoKey, resolveCloudBaseUrl } from "./client.js";
|
|
@@ -27,6 +28,30 @@ function defaultOpenBrowser(url) {
|
|
|
27
28
|
const { command, args } = browserOpenCommand(process.platform, url);
|
|
28
29
|
execFile(command, args, () => undefined); // best-effort: the printed URL is the fallback
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Write-preflight (0.4.1 E2E cert M4): prove `.env.local` is writable BEFORE
|
|
33
|
+
* any claim is opened or redeemed. Sandboxed agent runs — headless Claude
|
|
34
|
+
* Code protects env files even under --dangerously-skip-permissions — can
|
|
35
|
+
* deny the write; without this check the ceremony redeems the single-use
|
|
36
|
+
* claim, the key mints server-side, and the write failure loses it. A real
|
|
37
|
+
* append-mode open is the probe (permission checks like `access(W_OK)` don't
|
|
38
|
+
* see sandbox policies, which deny at open time); a probe-created empty file
|
|
39
|
+
* is removed again. Returns the failure detail, or null when writable.
|
|
40
|
+
*/
|
|
41
|
+
export async function preflightEnvLocalWrite(root) {
|
|
42
|
+
const path = join(root, ".env.local");
|
|
43
|
+
const existedBefore = await stat(path).then(() => true, () => false);
|
|
44
|
+
try {
|
|
45
|
+
const handle = await open(path, "a");
|
|
46
|
+
await handle.close();
|
|
47
|
+
if (!existedBefore)
|
|
48
|
+
await unlink(path).catch(() => undefined);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
30
55
|
function ceremonyFrom(value) {
|
|
31
56
|
const body = value;
|
|
32
57
|
if (typeof body?.claim_token !== "string"
|
|
@@ -92,6 +117,18 @@ export async function runDeviceLogin(args, options = {}) {
|
|
|
92
117
|
const waitMs = waitSeconds === undefined ? undefined : waitSeconds * 1000;
|
|
93
118
|
const pendingHome = options.home === undefined ? {} : { home: options.home };
|
|
94
119
|
const claimCwd = options.root ?? process.cwd();
|
|
120
|
+
// Prove the key's landing file is writable BEFORE the ceremony touches the
|
|
121
|
+
// network: a claim is single-use, so a post-approval write failure loses
|
|
122
|
+
// the minted key (M4). This is a WRITE failure, distinct from a timeout —
|
|
123
|
+
// the error says so instead of the generic polling copy.
|
|
124
|
+
const writeProblem = await preflightEnvLocalWrite(claimCwd);
|
|
125
|
+
if (writeProblem !== null) {
|
|
126
|
+
output.error(`Cannot write ${join(claimCwd, ".env.local")} (${writeProblem}). ` +
|
|
127
|
+
"`vendo login` lands the minted key there, so the ceremony was NOT started and no key was minted. " +
|
|
128
|
+
"This is a file-write problem, not a timeout: fix write access (sandboxed agent runs often deny env-file " +
|
|
129
|
+
"writes — re-run outside the sandbox or have your human run `vendo login`), then re-run.");
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
95
132
|
try {
|
|
96
133
|
// A still-open claim from a dead process (#479): resume polling it so the
|
|
97
134
|
// human's late approval — against the code they were already shown —
|
|
@@ -176,7 +213,21 @@ export async function runDeviceLogin(args, options = {}) {
|
|
|
176
213
|
if (typeof key !== "string" || !isVendoKey(key)) {
|
|
177
214
|
throw new Error("Vendo Cloud returned an invalid credential");
|
|
178
215
|
}
|
|
179
|
-
|
|
216
|
+
try {
|
|
217
|
+
await upsertEnvLocal(root, "VENDO_API_KEY", key);
|
|
218
|
+
}
|
|
219
|
+
catch (writeError) {
|
|
220
|
+
// The claim is consumed and the key minted — a resume can never
|
|
221
|
+
// succeed, so the pending file goes; and this must read as a WRITE
|
|
222
|
+
// failure, never the timeout/polling copy (M4). The key is never
|
|
223
|
+
// printed, so the minted key is unrecoverable here: name the
|
|
224
|
+
// console cleanup explicitly.
|
|
225
|
+
await deletePendingClaim(claimCwd, pendingHome);
|
|
226
|
+
throw new Error(`Approved and the key was minted, but writing it to ${join(root, ".env.local")} failed ` +
|
|
227
|
+
`(${writeError instanceof Error ? writeError.message : String(writeError)}). ` +
|
|
228
|
+
"The key was NOT saved and is not shown anywhere: revoke it in the console (Keys page), " +
|
|
229
|
+
"fix write access to .env.local, and run `vendo login` again.");
|
|
230
|
+
}
|
|
180
231
|
await deletePendingClaim(claimCwd, pendingHome);
|
|
181
232
|
// Never print the key itself — .env.local is the hand-off, last4 the
|
|
182
233
|
// receipt. A resumed run names the full path: it may differ from cwd.
|
|
@@ -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>;
|
package/dist/cli/dep-versions.js
CHANGED
|
@@ -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;
|
|
@@ -17,9 +17,13 @@ export declare const DOCTOR_ERROR_CODES: {
|
|
|
17
17
|
readonly "E-WIRE-003": "the Next.js catch-all handler app/api/vendo/[...vendo]/route.ts is missing";
|
|
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
|
+
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";
|
|
20
23
|
readonly "E-CFG-001": "a required .vendo/ config file is missing";
|
|
21
24
|
readonly "E-CFG-002": ".vendo/data/.gitignore is missing";
|
|
22
25
|
readonly "E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support";
|
|
26
|
+
readonly "E-DEP-002": "the running wire serves a different @vendoai/vendo version than this CLI (split-brain install)";
|
|
23
27
|
readonly "E-UI-001": "an ejected surface predates the installed @vendoai/ui";
|
|
24
28
|
readonly "E-DEV-001": "the dev server could not be started for the probe";
|
|
25
29
|
readonly "E-LIVE-001": "/status returned an invalid composition response";
|
|
@@ -27,6 +31,7 @@ export declare const DOCTOR_ERROR_CODES: {
|
|
|
27
31
|
readonly "E-LIVE-003": "/status returned an invalid execution venue";
|
|
28
32
|
readonly "E-LIVE-004": "no execution venue is configured";
|
|
29
33
|
readonly "E-LIVE-005": "the host /status does not report an execution venue (version skew)";
|
|
34
|
+
readonly "E-LIVE-006": "the app's root page returns a server error while the wire answers";
|
|
30
35
|
readonly "E-AUTH-001": "present credentials did not reach the host API";
|
|
31
36
|
readonly "E-AUTH-002": "the present credential probe is unreachable";
|
|
32
37
|
readonly "E-AUTH-003": "the present credential probe cannot run while the dev server is down";
|
|
@@ -46,6 +51,8 @@ export declare const DOCTOR_ERROR_CODES: {
|
|
|
46
51
|
readonly "E-TURN-001": "the live model turn did not answer";
|
|
47
52
|
readonly "E-TURN-002": "the live model turn cannot run while the dev server is down";
|
|
48
53
|
readonly "E-CLOUD-001": "VENDO_API_KEY is set but not usable";
|
|
54
|
+
readonly "E-TOOLS-001": "every extracted host tool is disabled or excluded (zero live host tools)";
|
|
55
|
+
readonly "E-TOOLS-002": "the extracted tool surface is empty (zero host tools)";
|
|
49
56
|
};
|
|
50
57
|
export type DoctorErrorCode = keyof typeof DOCTOR_ERROR_CODES;
|
|
51
58
|
/** Complete list of every code doctor can emit, for CI enumeration. */
|
package/dist/cli/doctor-codes.js
CHANGED
|
@@ -18,9 +18,13 @@ export const DOCTOR_ERROR_CODES = {
|
|
|
18
18
|
"E-WIRE-003": "the Next.js catch-all handler app/api/vendo/[...vendo]/route.ts is missing",
|
|
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
|
+
"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",
|
|
21
24
|
"E-CFG-001": "a required .vendo/ config file is missing",
|
|
22
25
|
"E-CFG-002": ".vendo/data/.gitignore is missing",
|
|
23
26
|
"E-DEP-001": "the installed ai package is a major version @vendoai/vendo does not support",
|
|
27
|
+
"E-DEP-002": "the running wire serves a different @vendoai/vendo version than this CLI (split-brain install)",
|
|
24
28
|
"E-UI-001": "an ejected surface predates the installed @vendoai/ui",
|
|
25
29
|
"E-DEV-001": "the dev server could not be started for the probe",
|
|
26
30
|
"E-LIVE-001": "/status returned an invalid composition response",
|
|
@@ -28,6 +32,7 @@ export const DOCTOR_ERROR_CODES = {
|
|
|
28
32
|
"E-LIVE-003": "/status returned an invalid execution venue",
|
|
29
33
|
"E-LIVE-004": "no execution venue is configured",
|
|
30
34
|
"E-LIVE-005": "the host /status does not report an execution venue (version skew)",
|
|
35
|
+
"E-LIVE-006": "the app's root page returns a server error while the wire answers",
|
|
31
36
|
"E-AUTH-001": "present credentials did not reach the host API",
|
|
32
37
|
"E-AUTH-002": "the present credential probe is unreachable",
|
|
33
38
|
"E-AUTH-003": "the present credential probe cannot run while the dev server is down",
|
|
@@ -47,6 +52,8 @@ export const DOCTOR_ERROR_CODES = {
|
|
|
47
52
|
"E-TURN-001": "the live model turn did not answer",
|
|
48
53
|
"E-TURN-002": "the live model turn cannot run while the dev server is down",
|
|
49
54
|
"E-CLOUD-001": "VENDO_API_KEY is set but not usable",
|
|
55
|
+
"E-TOOLS-001": "every extracted host tool is disabled or excluded (zero live host tools)",
|
|
56
|
+
"E-TOOLS-002": "the extracted tool surface is empty (zero host tools)",
|
|
50
57
|
};
|
|
51
58
|
/** Complete list of every code doctor can emit, for CI enumeration. */
|
|
52
59
|
export const doctorErrorCodes = Object.keys(DOCTOR_ERROR_CODES);
|
package/dist/cli/doctor-live.js
CHANGED
|
@@ -158,7 +158,17 @@ export async function startDevServerForProbe(options) {
|
|
|
158
158
|
log.push(`spawn error: ${error.message}\n`);
|
|
159
159
|
failSpawn();
|
|
160
160
|
});
|
|
161
|
-
|
|
161
|
+
// Destroy the piped streams too: `npm run dev` grandchildren (the actual
|
|
162
|
+
// next/vite process) can survive the SIGTERM and keep the inherited pipe
|
|
163
|
+
// fds open, and the live 'data' listeners would then hold doctor's event
|
|
164
|
+
// loop — the process would hang after printing its verdict instead of
|
|
165
|
+
// exiting with it (exit-code honesty, 0.4.1 E2E cert defect 13 family).
|
|
166
|
+
const stop = () => {
|
|
167
|
+
child.kill("SIGTERM");
|
|
168
|
+
child.stdout?.destroy();
|
|
169
|
+
child.stderr?.destroy();
|
|
170
|
+
child.unref?.(); // injected test doubles may not implement it
|
|
171
|
+
};
|
|
162
172
|
const up = await Promise.race([
|
|
163
173
|
waitForStatus(options.statusUrl, fetchImpl, options.timeoutMs ?? 120_000),
|
|
164
174
|
spawnFailed,
|
package/dist/cli/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { cloudDoctor, liveModelTurn, startDevServerForProbe, } from "./doctor-li
|
|
|
5
5
|
import { installedAiVersion } from "./dep-versions.js";
|
|
6
6
|
import { doctorFixRef } from "./doctor-codes.js";
|
|
7
7
|
import { EJECT_MANIFEST_FILE } from "./eject.js";
|
|
8
|
+
import { overridesFileSchema, toolsFileSchema } from "@vendoai/actions";
|
|
8
9
|
import { detectFramework, detectVendoWiring } from "./framework.js";
|
|
9
10
|
import { walk } from "./theme/walk.js";
|
|
10
11
|
import { remoteUrls, sameUrl, validateRegistryServer } from "./mcp/registry.js";
|
|
@@ -93,8 +94,40 @@ export async function runDoctor(options) {
|
|
|
93
94
|
const warn = (id, code, message) => { warnings += 1; checks.push({ id, status: "warning", message, error_code: code, fix_ref: doctorFixRef(code) }); if (!json)
|
|
94
95
|
output.error(`warning: ${message}`); };
|
|
95
96
|
const framework = await detectFramework(root);
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// The generated vendo/vendo-root.tsx wrapper carries the <VendoRoot> AND
|
|
98
|
+
// <VendoOverlay /> markers itself, so an unexcluded scan would pass the
|
|
99
|
+
// client/surface gates even when NO layout mounts the wrapper — the exact
|
|
100
|
+
// doctor-green-but-invisible failure E-WIRE-006 exists to catch. Mirror
|
|
101
|
+
// init's layout decision: exclude the wrapper from the scan, then let a
|
|
102
|
+
// user-code <VendoRoot> mount next to an overlay-bearing wrapper satisfy
|
|
103
|
+
// the surface (the wrapper renders the overlay once a layout mounts it).
|
|
104
|
+
const wrapperCandidates = [
|
|
105
|
+
join(root, "vendo", "vendo-root.tsx"),
|
|
106
|
+
join(root, "src", "vendo", "vendo-root.tsx"),
|
|
107
|
+
];
|
|
108
|
+
let wrapperWithOverlay = false;
|
|
109
|
+
for (const candidate of wrapperCandidates) {
|
|
110
|
+
const source = await readFile(candidate, "utf8").catch(() => null);
|
|
111
|
+
if (source !== null && source.includes("<VendoOverlay"))
|
|
112
|
+
wrapperWithOverlay = true;
|
|
113
|
+
}
|
|
114
|
+
const scanned = await detectVendoWiring(root, { exclude: wrapperCandidates });
|
|
115
|
+
const wiring = { ...scanned, surface: scanned.surface || (scanned.client && wrapperWithOverlay) };
|
|
116
|
+
if (framework === "unknown") {
|
|
117
|
+
// No framework to pattern-match (field case: a Cloudflare Worker + Vite
|
|
118
|
+
// host failed E-WIRE-003/004 forever) — judge the wiring by the same
|
|
119
|
+
// bounded source scan init uses, never by another framework's file
|
|
120
|
+
// layout. The surface check below still runs; it is source-generic.
|
|
121
|
+
if (scanned.server)
|
|
122
|
+
pass("wiring/server", "createVendo server wiring found");
|
|
123
|
+
else
|
|
124
|
+
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");
|
|
125
|
+
if (scanned.client)
|
|
126
|
+
pass("wiring/client", "<VendoRoot> wraps the client");
|
|
127
|
+
else
|
|
128
|
+
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");
|
|
129
|
+
}
|
|
130
|
+
else if (framework === "express") {
|
|
98
131
|
if (wiring.server)
|
|
99
132
|
pass("wiring/express-server", "Express server is wired");
|
|
100
133
|
else
|
|
@@ -113,21 +146,32 @@ export async function runDoctor(options) {
|
|
|
113
146
|
pass("wiring/next-route", "catch-all handler is wired");
|
|
114
147
|
else
|
|
115
148
|
fail("wiring/next-route", "E-WIRE-003", "missing app/api/vendo/[...vendo]/route.ts");
|
|
116
|
-
|
|
149
|
+
// The mount may live in ANY layout, not just the root one (i18n/route-group
|
|
150
|
+
// hosts mount in e.g. app/[locale]/layout.tsx — the literal root-layout
|
|
151
|
+
// grep fought exactly that correct wiring in the 0.4.1 E2E cert), and the
|
|
152
|
+
// correct scaffold mounts the generated vendo/vendo-root.tsx wrapper —
|
|
153
|
+
// whose export is also named VendoRoot, so the marker holds there too.
|
|
117
154
|
let rootWired = false;
|
|
118
|
-
for (const
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
for (const appDir of [join(root, "app"), join(root, "src", "app")]) {
|
|
156
|
+
for (const path of await walk(appDir, (rel) => /(^|[\\/])layout\.(?:tsx|jsx|js)$/.test(rel))) {
|
|
157
|
+
const source = await readFile(path, "utf8").catch(() => "");
|
|
158
|
+
if (source.includes("<VendoRoot") || source.includes("<VendoProvider"))
|
|
121
159
|
rootWired = true;
|
|
122
160
|
}
|
|
123
|
-
catch {
|
|
124
|
-
// Try the other layout convention.
|
|
125
|
-
}
|
|
126
161
|
}
|
|
127
162
|
if (rootWired)
|
|
128
163
|
pass("wiring/next-root", "<VendoRoot> wraps the app");
|
|
129
164
|
else
|
|
130
|
-
fail("wiring/next-root", "E-WIRE-004", "
|
|
165
|
+
fail("wiring/next-root", "E-WIRE-004", "no app layout mounts <VendoRoot> — wrap the app in the generated vendo/vendo-root.tsx wrapper (its export is also named VendoRoot), in the root layout or any layout that covers your pages");
|
|
166
|
+
}
|
|
167
|
+
// Visible surface (0.4.1 E2E cert B3): <VendoRoot> is a context provider
|
|
168
|
+
// that renders NOTHING — two certified stacks ended doctor-green with no
|
|
169
|
+
// way for a user to reach the agent. Green must mean visible.
|
|
170
|
+
if (wiring.surface) {
|
|
171
|
+
pass("wiring/surface", "a visible agent surface is mounted (<VendoOverlay /> or an equivalent)");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
fail("wiring/surface", "E-WIRE-006", "no visible agent surface is mounted — <VendoRoot> renders nothing by itself; mount <VendoOverlay /> (init generates vendo/vendo-root.tsx for this), or render your own surface (<VendoThread />, <VendoToolResult>, the BYO embeds)");
|
|
131
175
|
}
|
|
132
176
|
if (await hasDependency(root))
|
|
133
177
|
pass("wiring/dependency", "@vendoai/vendo dependency is declared");
|
|
@@ -155,6 +199,42 @@ export async function runDoctor(options) {
|
|
|
155
199
|
}
|
|
156
200
|
if (!await exists(join(root, ".vendo", "data", ".gitignore")))
|
|
157
201
|
warn("config/data-gitignore", "E-CFG-002", ".vendo/data/.gitignore is missing");
|
|
202
|
+
// The core promise, statically checkable: does the agent have any HOST
|
|
203
|
+
// tool it may actually call? All-disabled is an explicit misconfiguration
|
|
204
|
+
// (fail); an empty extraction is a strong warning — connector-only hosts
|
|
205
|
+
// are legitimate, but a fresh install landing here means extraction found
|
|
206
|
+
// nothing user-facing (field case: an infra product whose surface was all
|
|
207
|
+
// internal endpoints ended with tools: [] and a silently useless agent).
|
|
208
|
+
const toolsRaw = await readOptional(join(root, ".vendo", "tools.json"));
|
|
209
|
+
const overridesRaw = await readOptional(join(root, ".vendo", "overrides.json"));
|
|
210
|
+
if (toolsRaw !== null) {
|
|
211
|
+
try {
|
|
212
|
+
const toolsFile = toolsFileSchema.parse(JSON.parse(toolsRaw));
|
|
213
|
+
let overridesTools = {};
|
|
214
|
+
if (overridesRaw !== null) {
|
|
215
|
+
try {
|
|
216
|
+
overridesTools = overridesFileSchema.parse(JSON.parse(overridesRaw)).tools;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Malformed overrides are their own (pre-existing) failure surface.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const live = toolsFile.tools.filter((tool) => (overridesTools[tool.name]?.disabled ?? tool.disabled ?? false) !== true);
|
|
223
|
+
if (toolsFile.tools.length === 0) {
|
|
224
|
+
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)");
|
|
225
|
+
}
|
|
226
|
+
else if (live.length === 0) {
|
|
227
|
+
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)`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
pass("tools/live-surface", `${live.length} live host tool${live.length === 1 ? "" : "s"}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Not the vendo/tools@1 shape (e.g. a placeholder {}) — the config
|
|
235
|
+
// checks above already govern presence; nothing to grade here.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
158
238
|
// §4 customization ladder — ejected chrome drift. The ejected pixels are the
|
|
159
239
|
// host's code, so a version gap is awareness (warn), never breakage (fail):
|
|
160
240
|
// the hooks/wire dependency keeps working; only new presentation is missed.
|
|
@@ -235,6 +315,18 @@ export async function runDoctor(options) {
|
|
|
235
315
|
else {
|
|
236
316
|
pass("live/status", `/status live round-trip (${body.version}, ${body.posture})`);
|
|
237
317
|
liveComposition = true;
|
|
318
|
+
// Split-brain guard (0.4.2 re-run, invoify defect 13): a direct
|
|
319
|
+
// @vendoai/vendo dependency pinned to an older range beats the vendoai
|
|
320
|
+
// umbrella's for the APP import, so `npm install vendoai@latest` runs a
|
|
321
|
+
// new CLI while /status silently serves the old runtime. Any CLI/wire
|
|
322
|
+
// version disagreement — split-brain or just a dev server started
|
|
323
|
+
// before the upgrade — means doctor is not certifying what users run.
|
|
324
|
+
if (body.version === CLI_VERSION) {
|
|
325
|
+
pass("deps/version-skew", `CLI and running wire agree on @vendoai/vendo ${CLI_VERSION}`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
fail("deps/version-skew", "E-DEP-002", `the running wire serves @vendoai/vendo ${body.version} but this CLI is ${CLI_VERSION} — likely a split-brain install (a direct @vendoai/vendo dependency pinned to an older range wins over the vendoai umbrella's). Fix: npm install @vendoai/vendo@${CLI_VERSION} (or remove the direct @vendoai/vendo dependency and reinstall), then restart the dev server and re-run doctor.`);
|
|
329
|
+
}
|
|
238
330
|
// 10-mcp §1 — the door flag lives under blocks.mcp.
|
|
239
331
|
mcpEnabled = body.blocks.mcp === true;
|
|
240
332
|
sandboxVenue = body.blocks.sandbox;
|
|
@@ -254,7 +346,26 @@ export async function runDoctor(options) {
|
|
|
254
346
|
}
|
|
255
347
|
}
|
|
256
348
|
catch {
|
|
257
|
-
fail("live/status", "E-LIVE-002", `/status is unreachable at ${statusUrl}/status`);
|
|
349
|
+
fail("live/status", "E-LIVE-002", `/status is unreachable at ${statusUrl}/status — doctor expects the WIRE BASE (your app origin plus the mount path, e.g. http://localhost:3000/api/vendo); a bare site origin passed to --url is missing the /api/vendo part`);
|
|
350
|
+
}
|
|
351
|
+
// Render gate (0.4.1 E2E cert M3): a live wire proves nothing about the
|
|
352
|
+
// PAGES — the certified invoify install had every page 500ing (registry
|
|
353
|
+
// passed across the Server Component boundary) while doctor exited 0. One
|
|
354
|
+
// cheap GET of the app root catches a site that is down for users.
|
|
355
|
+
if (liveComposition) {
|
|
356
|
+
try {
|
|
357
|
+
const response = await fetchImpl(`${new URL(statusUrl).origin}/`, { headers: { accept: "text/html" } });
|
|
358
|
+
if (response.status >= 500) {
|
|
359
|
+
fail("live/render", "E-LIVE-006", `the app's root page returned ${response.status} — the site is crashing for users even though the wire answers (typical cause: the component registry imported in a Server Component layout; mount it via the generated vendo/vendo-root.tsx wrapper instead). Check the dev server log.`);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
pass("live/render", `the app's root page renders (HTTP ${response.status})`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// The wire answered but the origin root didn't resolve at all — hosts
|
|
367
|
+
// that serve no page at / are not doctor's business; skip silently.
|
|
368
|
+
}
|
|
258
369
|
}
|
|
259
370
|
if (!liveComposition) {
|
|
260
371
|
fail("auth/present", "E-AUTH-003", `present credential probe cannot run; start the dev server at ${statusUrl} and retry`);
|
|
@@ -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
|
-
|
|
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({
|