aui-agent-builder 0.3.90 → 0.3.92
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/api-client/index.d.ts +68 -0
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +69 -1
- package/dist/api-client/index.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +64 -1
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/validate.d.ts +41 -9
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +501 -846
- package/dist/commands/validate.js.map +1 -1
- package/dist/telemetry.d.ts +21 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +46 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/utils/request-capture.d.ts +26 -0
- package/dist/utils/request-capture.d.ts.map +1 -1
- package/dist/utils/request-capture.js +67 -0
- package/dist/utils/request-capture.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,168 +1,310 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* validate command
|
|
3
|
+
* validate command — Validate AUI files against the agent-settings backend.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* ─── HOW THIS COMMAND WORKS NOW ─────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Validation is performed REMOTELY against
|
|
8
|
+
* POST {agent-settings}/v1/agents/{agent_id}/validate
|
|
9
|
+
*
|
|
10
|
+
* The CLI reads every `.aui.json` file in the project, assembles the flat
|
|
11
|
+
* payload the backend expects (general_settings + parameters + entities +
|
|
12
|
+
* rules + integrations + tools), and forwards it to the backend's
|
|
13
|
+
* canonical pydantic + cross-reference validator. The response is mapped
|
|
14
|
+
* back onto local file paths so the existing `ValidateView` can render
|
|
15
|
+
* the errors/warnings exactly as it did when validation ran locally.
|
|
16
|
+
*
|
|
17
|
+
* The previous local implementation (AJV + JS cross-reference checks) is
|
|
18
|
+
* preserved INLINE BELOW inside the `LEGACY_LOCAL_VALIDATION_*` regions
|
|
19
|
+
* as a JS comment so we can revive it for offline or backend-down
|
|
20
|
+
* scenarios without re-deriving the logic from git history. Do not delete
|
|
21
|
+
* those regions — they are a deliberate fall-back snapshot.
|
|
22
|
+
*
|
|
23
|
+
* ─── PUSH FLOW SAFETY ───────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* `aui push` invokes `validate(projectRoot)` in its preflight span and
|
|
26
|
+
* aborts the push (with `ValidationError`) whenever validation returns
|
|
27
|
+
* false — UNLESS `--force` is set. We preserve that contract here: any
|
|
28
|
+
* validation error (backend-reported or infra failure) returns false.
|
|
29
|
+
*
|
|
30
|
+
* • Backend says `valid: false` → return false (push blocked)
|
|
31
|
+
* • Backend returns 4xx/5xx → return false (push blocked)
|
|
32
|
+
* • Network/timeout failure → return false (push blocked)
|
|
33
|
+
* • Backend returns `valid: true` → return true (push proceeds)
|
|
34
|
+
*
|
|
35
|
+
* Push-time safety is thus exactly what it was before: a failed
|
|
36
|
+
* validation never silently lets a push through.
|
|
12
37
|
*
|
|
13
38
|
* Usage: aui validate [path]
|
|
14
39
|
* Options:
|
|
15
40
|
* --verbose Show detailed output
|
|
41
|
+
* --strict Reserved (no-op against the remote validator today;
|
|
42
|
+
* kept so existing scripts that pass it don't break)
|
|
16
43
|
*/
|
|
17
44
|
import * as fs from "fs";
|
|
18
45
|
import * as path from "path";
|
|
19
46
|
import { fileURLToPath } from "url";
|
|
20
|
-
import { execSync } from "child_process";
|
|
21
47
|
import { render } from "ink";
|
|
22
|
-
import AjvModule from "ajv";
|
|
23
|
-
const Ajv = AjvModule.default ?? AjvModule;
|
|
24
48
|
import { glob } from "glob";
|
|
25
|
-
import { findProjectRoot } from "../config/index.js";
|
|
26
|
-
import { parseAuiFile,
|
|
49
|
+
import { findProjectRoot, loadProjectConfig, loadSession, getConfig, } from "../config/index.js";
|
|
50
|
+
import { parseAuiFile, } from "../utils/index.js";
|
|
27
51
|
import { ValidateView } from "../ui/views/ValidateView.js";
|
|
28
52
|
import { ValidationError } from "../errors/index.js";
|
|
29
53
|
import { isJsonMode, outputJson } from "../utils/json-output.js";
|
|
30
|
-
import { getTracer, SpanStatusCode, setUserContext } from "../telemetry.js";
|
|
54
|
+
import { getTracer, SpanStatusCode, setUserContext, setAgentContext, } from "../telemetry.js";
|
|
55
|
+
import { AUIClient, AUIAPIError } from "../api-client/index.js";
|
|
31
56
|
const __filename = fileURLToPath(import.meta.url);
|
|
32
57
|
const __dirname = path.dirname(__filename);
|
|
58
|
+
/** Schema-version pinned by the backend validator. */
|
|
59
|
+
const VALIDATE_SCHEMA_VERSION = 1;
|
|
33
60
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
61
|
+
* Map a backend section name to the local file name we surface in the UI.
|
|
62
|
+
* Tools are handled separately — each tool lives in its own file under
|
|
63
|
+
* `tools/<code>.aui.json` and we resolve the code via the payload index.
|
|
37
64
|
*/
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const TYPE_TO_KEY = {
|
|
45
|
-
tools: "tool",
|
|
46
|
-
agent: "agent",
|
|
47
|
-
parameters: "parameters",
|
|
48
|
-
entities: "entities",
|
|
49
|
-
integrations: "integrations",
|
|
50
|
-
rules: "rules",
|
|
51
|
-
// "knowledge-bases": null,
|
|
52
|
-
// "knowledge-bases-files": null,
|
|
65
|
+
const SECTION_TO_FILE = {
|
|
66
|
+
general_settings: "agent.aui.json",
|
|
67
|
+
parameters: "parameters.aui.json",
|
|
68
|
+
entities: "entities.aui.json",
|
|
69
|
+
rules: "rules.aui.json",
|
|
70
|
+
integrations: "integrations.aui.json",
|
|
53
71
|
};
|
|
54
72
|
/**
|
|
55
|
-
* Discover all
|
|
56
|
-
*
|
|
57
|
-
* for any *.dschema.json files. Later matches override earlier ones (local wins).
|
|
73
|
+
* Discover all `.aui.json` files (excluding any helper / cache dirs).
|
|
74
|
+
* Returns absolute paths.
|
|
58
75
|
*/
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const bundledDir = path.resolve(__dirname, "..", "schema");
|
|
63
|
-
if (fs.existsSync(bundledDir)) {
|
|
64
|
-
for (const file of fs.readdirSync(bundledDir)) {
|
|
65
|
-
const match = file.match(/^(.+)\.dschema\.json$/);
|
|
66
|
-
if (match) {
|
|
67
|
-
schemas.set(match[1], path.join(bundledDir, file));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// 2. Scan searchRoot recursively for any *.dschema.json
|
|
72
|
-
const localFiles = await glob("**/*.dschema.json", {
|
|
73
|
-
cwd: searchRoot,
|
|
76
|
+
async function discoverAuiFiles(projectRoot) {
|
|
77
|
+
return glob("**/*.aui.json", {
|
|
78
|
+
cwd: projectRoot,
|
|
74
79
|
absolute: true,
|
|
75
80
|
ignore: ["node_modules/**", ".aui/**", ".aui-cache/**"],
|
|
76
81
|
});
|
|
77
|
-
for (const filePath of localFiles) {
|
|
78
|
-
const base = path.basename(filePath);
|
|
79
|
-
const match = base.match(/^(.+)\.dschema\.json$/);
|
|
80
|
-
if (match) {
|
|
81
|
-
schemas.set(match[1], filePath);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return schemas;
|
|
85
82
|
}
|
|
86
83
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
84
|
+
* Read every project file and build the flat payload expected by
|
|
85
|
+
* `POST /v1/agents/{id}/validate`. Returns the payload plus an index
|
|
86
|
+
* mapping backend section indexes back to local file names so we can
|
|
87
|
+
* surface accurate file paths in the UI.
|
|
88
|
+
*
|
|
89
|
+
* Files that fail to parse are NOT included in the payload — they are
|
|
90
|
+
* returned in `parseErrors` and surfaced as local validation issues so
|
|
91
|
+
* the backend doesn't choke on syntactically invalid JSON.
|
|
90
92
|
*/
|
|
91
|
-
async function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
async function buildValidatePayload(projectRoot) {
|
|
94
|
+
const files = await discoverAuiFiles(projectRoot);
|
|
95
|
+
const parseErrors = [];
|
|
96
|
+
const validatedFiles = [];
|
|
97
|
+
const payload = {
|
|
98
|
+
schema_version: VALIDATE_SCHEMA_VERSION,
|
|
99
|
+
};
|
|
100
|
+
const toolFiles = [];
|
|
101
|
+
// Sort tool files alphabetically by basename so the payload index is
|
|
102
|
+
// deterministic between runs (important for diffing & log correlation).
|
|
103
|
+
const toolFilesAll = [];
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
const rel = path.relative(projectRoot, f);
|
|
106
|
+
if (rel.startsWith("tools/") || rel.startsWith("tools\\")) {
|
|
107
|
+
toolFilesAll.push(f);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
toolFilesAll.sort();
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const rel = path.relative(projectRoot, file);
|
|
113
|
+
const parsed = parseAuiFile(file);
|
|
114
|
+
if (!parsed) {
|
|
115
|
+
parseErrors.push({
|
|
116
|
+
type: "error",
|
|
117
|
+
file: rel,
|
|
118
|
+
message: "Invalid JSON syntax — file could not be parsed",
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const isToolFile = rel.startsWith("tools/") || rel.startsWith("tools\\");
|
|
123
|
+
if (isToolFile) {
|
|
124
|
+
// Handled later via toolFilesAll to keep deterministic index order
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (rel === "agent.aui.json" || parsed.general_settings) {
|
|
128
|
+
const gs = (parsed.general_settings ?? parsed);
|
|
129
|
+
payload.general_settings = gs;
|
|
130
|
+
validatedFiles.push(rel);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (parsed.parameters && Array.isArray(parsed.parameters)) {
|
|
134
|
+
payload.parameters = parsed.parameters;
|
|
135
|
+
validatedFiles.push(rel);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (parsed.entities && Array.isArray(parsed.entities)) {
|
|
139
|
+
payload.entities = parsed.entities;
|
|
140
|
+
validatedFiles.push(rel);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (parsed.integrations && Array.isArray(parsed.integrations)) {
|
|
144
|
+
payload.integrations = parsed.integrations;
|
|
145
|
+
validatedFiles.push(rel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (parsed.rules && Array.isArray(parsed.rules)) {
|
|
149
|
+
payload.rules = parsed.rules;
|
|
150
|
+
validatedFiles.push(rel);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Build tools array preserving deterministic ordering.
|
|
155
|
+
const tools = [];
|
|
156
|
+
for (const file of toolFilesAll) {
|
|
157
|
+
const rel = path.relative(projectRoot, file);
|
|
158
|
+
const parsed = parseAuiFile(file);
|
|
159
|
+
if (!parsed) {
|
|
160
|
+
// Already captured above in parseErrors. Skip to keep array dense.
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const toolObj = (parsed.tool ?? parsed);
|
|
164
|
+
tools.push(toolObj);
|
|
165
|
+
toolFiles.push(rel);
|
|
166
|
+
validatedFiles.push(rel);
|
|
167
|
+
}
|
|
168
|
+
if (tools.length > 0) {
|
|
169
|
+
payload.tools = tools;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
payload,
|
|
173
|
+
index: {
|
|
174
|
+
toolFiles,
|
|
175
|
+
validatedFiles,
|
|
176
|
+
totalFiles: validatedFiles.length,
|
|
177
|
+
},
|
|
178
|
+
parseErrors,
|
|
179
|
+
};
|
|
105
180
|
}
|
|
106
181
|
/**
|
|
107
|
-
* Convert
|
|
182
|
+
* Convert a single backend `AgentValidateIssue` into the CLI's
|
|
183
|
+
* `ValidationIssue` shape so existing UI/JSON consumers don't see a new
|
|
184
|
+
* envelope. `loc` is rendered as a dot-path with bracket indexes for
|
|
185
|
+
* arrays — matches what FastAPI prints in its docs and what users will
|
|
186
|
+
* see in their browser's network tab if they ever inspect the endpoint
|
|
187
|
+
* directly.
|
|
108
188
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
function backendIssueToValidationIssue(issue, file, sectionPath) {
|
|
190
|
+
const severity = issue.severity === "warning" ? "warning" : "error";
|
|
191
|
+
const loc = (issue.loc ?? [])
|
|
192
|
+
.map((p) => (typeof p === "number" ? `[${p}]` : `.${p}`))
|
|
193
|
+
.join("")
|
|
194
|
+
.replace(/^\./, "");
|
|
195
|
+
const fullPath = loc ? `${sectionPath}.${loc}`.replace(/\.\[/g, "[") : sectionPath;
|
|
196
|
+
const code = issue.code ? `[${issue.code}] ` : "";
|
|
197
|
+
return {
|
|
198
|
+
type: severity,
|
|
199
|
+
file,
|
|
200
|
+
path: fullPath,
|
|
201
|
+
message: `${code}${issue.message}${loc ? ` (at ${loc})` : ""}`,
|
|
202
|
+
};
|
|
118
203
|
}
|
|
119
204
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
205
|
+
* Map a per-section result onto local file paths.
|
|
206
|
+
*
|
|
207
|
+
* `general_settings` is a single object (no index). All other sections
|
|
208
|
+
* are arrays where each element carries its own index into the original
|
|
209
|
+
* payload. Tools additionally need their code looked up via the payload
|
|
210
|
+
* index because each tool lives in its own file on disk.
|
|
124
211
|
*/
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
212
|
+
function mapResponseToIssues(response, payloadIndex) {
|
|
213
|
+
const issues = [];
|
|
214
|
+
// ── general_settings ──
|
|
215
|
+
const gs = response.general_settings;
|
|
216
|
+
if (gs) {
|
|
217
|
+
const file = SECTION_TO_FILE.general_settings;
|
|
218
|
+
for (const err of gs.errors ?? []) {
|
|
219
|
+
issues.push(backendIssueToValidationIssue({ ...err, severity: err.severity ?? "error" }, file, "general_settings"));
|
|
220
|
+
}
|
|
221
|
+
for (const w of gs.warnings ?? []) {
|
|
222
|
+
issues.push(backendIssueToValidationIssue({ ...w, severity: w.severity ?? "warning" }, file, "general_settings"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── array sections (parameters, entities, rules, integrations) ──
|
|
226
|
+
const arraySections = [
|
|
227
|
+
["parameters", "parameters"],
|
|
228
|
+
["entities", "entities"],
|
|
229
|
+
["rules", "rules"],
|
|
230
|
+
["integrations", "integrations"],
|
|
231
|
+
];
|
|
232
|
+
for (const [key, section] of arraySections) {
|
|
233
|
+
const items = response[key];
|
|
234
|
+
if (!Array.isArray(items))
|
|
131
235
|
continue;
|
|
132
|
-
|
|
133
|
-
|
|
236
|
+
const file = SECTION_TO_FILE[section];
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
const pathPrefix = `${section}[${item.index}]`;
|
|
239
|
+
for (const err of item.result.errors ?? []) {
|
|
240
|
+
issues.push(backendIssueToValidationIssue({ ...err, severity: err.severity ?? "error" }, file, pathPrefix));
|
|
241
|
+
}
|
|
242
|
+
for (const w of item.result.warnings ?? []) {
|
|
243
|
+
issues.push(backendIssueToValidationIssue({ ...w, severity: w.severity ?? "warning" }, file, pathPrefix));
|
|
244
|
+
}
|
|
134
245
|
}
|
|
135
246
|
}
|
|
247
|
+
// ── agent_tools (one-file-per-tool) ──
|
|
248
|
+
const tools = response.agent_tools;
|
|
249
|
+
if (Array.isArray(tools)) {
|
|
250
|
+
for (const item of tools) {
|
|
251
|
+
const toolFile = payloadIndex.toolFiles[item.index] ?? `tools[${item.index}]`;
|
|
252
|
+
for (const err of item.result.errors ?? []) {
|
|
253
|
+
issues.push(backendIssueToValidationIssue({ ...err, severity: err.severity ?? "error" }, toolFile, "tool"));
|
|
254
|
+
}
|
|
255
|
+
for (const w of item.result.warnings ?? []) {
|
|
256
|
+
issues.push(backendIssueToValidationIssue({ ...w, severity: w.severity ?? "warning" }, toolFile, "tool"));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return issues;
|
|
136
261
|
}
|
|
137
262
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
263
|
+
* Render results into the existing `ValidateView` envelope. We split
|
|
264
|
+
* issues into `schemaIssues` (issues with a specific path inside a
|
|
265
|
+
* section, i.e. backend schema validation) and `crossRefIssues` (no
|
|
266
|
+
* sub-path → cross-section references, the legacy local code's
|
|
267
|
+
* categorization). This keeps the UI grouping the user already knows.
|
|
140
268
|
*/
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
269
|
+
function categorizeIssues(issues) {
|
|
270
|
+
const schemaIssues = [];
|
|
271
|
+
const crossRefIssues = [];
|
|
272
|
+
for (const issue of issues) {
|
|
273
|
+
// Heuristic: backend issues with a non-trivial path go into "schema"
|
|
274
|
+
// (validator caught a specific field). Issues whose path is the bare
|
|
275
|
+
// section name represent cross-references the validator surfaces as
|
|
276
|
+
// "missing reference foo" / "duplicate code bar" — group them as
|
|
277
|
+
// cross-reference issues so the user sees the same two-section UI
|
|
278
|
+
// they had with local validation.
|
|
279
|
+
const path = issue.path ?? "";
|
|
280
|
+
if (path.includes(".") || path.includes("[")) {
|
|
281
|
+
schemaIssues.push(issue);
|
|
148
282
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
collectConditions(obj[key], out);
|
|
152
|
-
}
|
|
283
|
+
else {
|
|
284
|
+
crossRefIssues.push(issue);
|
|
153
285
|
}
|
|
154
286
|
}
|
|
287
|
+
return { schemaIssues, crossRefIssues };
|
|
155
288
|
}
|
|
156
289
|
/**
|
|
157
|
-
* Validate AUI configuration files against
|
|
290
|
+
* Validate AUI configuration files against the agent-settings backend.
|
|
291
|
+
*
|
|
292
|
+
* Returns `true` only when the backend explicitly reports `valid: true`
|
|
293
|
+
* with no error-severity issues. Any other outcome (errors, network
|
|
294
|
+
* failure, missing config) returns `false` so `aui push` blocks the
|
|
295
|
+
* push unless the user passes `--force`.
|
|
158
296
|
*/
|
|
159
297
|
export async function validate(targetPath, options = {}) {
|
|
160
298
|
const tracer = getTracer();
|
|
161
299
|
return tracer.startActiveSpan("aui.validate", async (span) => {
|
|
162
|
-
setUserContext(span);
|
|
300
|
+
await setUserContext(span);
|
|
163
301
|
try {
|
|
164
|
-
const result = await _validate(targetPath, options);
|
|
165
|
-
span.
|
|
302
|
+
const result = await _validate(span, targetPath, options);
|
|
303
|
+
span.setAttribute("validate.result.valid", result);
|
|
304
|
+
span.setStatus({
|
|
305
|
+
code: result ? SpanStatusCode.OK : SpanStatusCode.ERROR,
|
|
306
|
+
message: result ? undefined : "Validation failed",
|
|
307
|
+
});
|
|
166
308
|
return result;
|
|
167
309
|
}
|
|
168
310
|
catch (error) {
|
|
@@ -178,773 +320,286 @@ export async function validate(targetPath, options = {}) {
|
|
|
178
320
|
}
|
|
179
321
|
});
|
|
180
322
|
}
|
|
181
|
-
async function _validate(targetPath, options = {}) {
|
|
323
|
+
async function _validate(parentSpan, targetPath, options = {}) {
|
|
182
324
|
const projectRoot = targetPath || findProjectRoot() || process.cwd();
|
|
325
|
+
parentSpan.setAttribute("validate.project_root", projectRoot);
|
|
183
326
|
if (targetPath && !fs.existsSync(targetPath)) {
|
|
184
327
|
throw new ValidationError(`Path does not exist: ${targetPath}`, {
|
|
185
328
|
suggestion: "Provide a valid directory path, or run from an AUI project directory.",
|
|
186
329
|
});
|
|
187
330
|
}
|
|
188
|
-
//
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// (typeName === "knowledge-bases" || typeName === "knowledge-bases-files") &&
|
|
247
|
-
// Array.isArray((parsed as Record<string, unknown>).resources) &&
|
|
248
|
-
// ((parsed as Record<string, unknown>).resources as unknown[]).length === 0
|
|
249
|
-
// ) {
|
|
250
|
-
// continue;
|
|
251
|
-
// }
|
|
252
|
-
if (Array.isArray(dataToValidate)) {
|
|
253
|
-
for (let i = 0; i < dataToValidate.length; i++) {
|
|
254
|
-
const valid = validateFn(dataToValidate[i]);
|
|
255
|
-
if (!valid) {
|
|
256
|
-
issues.push(...ajvErrorsToIssues(validateFn.errors, relativePath, issueType).map((issue) => ({
|
|
257
|
-
...issue,
|
|
258
|
-
path: `${dataKey}[${i}]${issue.path === "/" ? "" : issue.path}`,
|
|
259
|
-
})));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
const valid = validateFn(dataToValidate);
|
|
265
|
-
if (!valid) {
|
|
266
|
-
issues.push(...ajvErrorsToIssues(validateFn.errors, relativePath, issueType));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Mark the boundary between schema and cross-reference issues
|
|
272
|
-
const schemaIssueCount = issues.length;
|
|
273
|
-
// 3. Cross-reference: ensure all params in integrations/entities exist in parameters
|
|
274
|
-
const agentData = await readAgentFiles(projectRoot);
|
|
275
|
-
// 3a. Rules: if a condition has more than 1 param, operation must be any_has_value or all_has_value
|
|
276
|
-
const MULTI_PARAM_OPERATIONS = ["any_has_value", "all_has_value"];
|
|
277
|
-
const rulesRaw = agentData.rules;
|
|
278
|
-
const rulesList = Array.isArray(rulesRaw)
|
|
279
|
-
? rulesRaw
|
|
280
|
-
: [];
|
|
281
|
-
for (const rule of rulesList) {
|
|
282
|
-
const conditions = [];
|
|
283
|
-
const when = rule.when;
|
|
284
|
-
if (when) {
|
|
285
|
-
for (const key of ["all", "any", "not"]) {
|
|
286
|
-
const items = when[key];
|
|
287
|
-
if (Array.isArray(items)) {
|
|
288
|
-
collectConditions(items, conditions);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
for (const cond of conditions) {
|
|
293
|
-
const params = cond.params;
|
|
294
|
-
const operation = cond.operation;
|
|
295
|
-
if (params && params.length > 1) {
|
|
296
|
-
if (operation && !MULTI_PARAM_OPERATIONS.includes(operation)) {
|
|
297
|
-
issues.push({
|
|
298
|
-
type: "error",
|
|
299
|
-
file: "rules.aui.json",
|
|
300
|
-
message: `Rule "${rule.code}" has a condition with ${params.length} params [${params.join(", ")}] but operation "${operation}" — when params has more than 1 value, operation must be one of: ${MULTI_PARAM_OPERATIONS.join(", ")}`,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
if (cond.value !== null && cond.value !== undefined) {
|
|
304
|
-
issues.push({
|
|
305
|
-
type: "error",
|
|
306
|
-
file: "rules.aui.json",
|
|
307
|
-
message: `Rule "${rule.code}" has a condition with ${params.length} params [${params.join(", ")}] but value is "${cond.value}" — when params has more than 1 value, value must be null`,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// 3b. Tool rules: when with empty combinator (e.g. { all: [] }) should be null
|
|
314
|
-
const toolFiles = await findFilesForType(projectRoot, "tools");
|
|
315
|
-
for (const file of toolFiles) {
|
|
316
|
-
const parsed = parseAuiFile(file);
|
|
317
|
-
if (!parsed?.tool)
|
|
318
|
-
continue;
|
|
319
|
-
const tool = parsed.tool;
|
|
320
|
-
const relativePath = path.relative(projectRoot, file);
|
|
321
|
-
for (const section of ["pre", "summary"]) {
|
|
322
|
-
const ruleSection = tool.rules?.[section];
|
|
323
|
-
if (!ruleSection)
|
|
324
|
-
continue;
|
|
325
|
-
for (const [category, ruleArray] of Object.entries(ruleSection)) {
|
|
326
|
-
if (!Array.isArray(ruleArray))
|
|
327
|
-
continue;
|
|
328
|
-
for (const rule of ruleArray) {
|
|
329
|
-
const r = rule;
|
|
330
|
-
// when: null is only allowed in summary rules
|
|
331
|
-
if (r.when === null && section !== "summary") {
|
|
332
|
-
issues.push({
|
|
333
|
-
type: "error",
|
|
334
|
-
file: relativePath,
|
|
335
|
-
message: `Tool "${tool.code}" has a ${section}.${category} rule with when: null — when: null is only allowed in summary rules. Non-summary rules must have a condition.`,
|
|
336
|
-
});
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
if (r.when && typeof r.when === "object") {
|
|
340
|
-
const keys = Object.keys(r.when);
|
|
341
|
-
const isEmpty = keys.length === 0 ||
|
|
342
|
-
keys.every((k) => {
|
|
343
|
-
const v = r.when[k];
|
|
344
|
-
return Array.isArray(v) && v.length === 0;
|
|
345
|
-
});
|
|
346
|
-
if (isEmpty) {
|
|
347
|
-
issues.push({
|
|
348
|
-
type: "error",
|
|
349
|
-
file: relativePath,
|
|
350
|
-
message: `Tool "${tool.code}" has a ${section}.${category} rule with an empty "when" clause (e.g. { all: [] }) — ${section === "summary" ? "use when: null instead" : "non-summary rules must have a condition"}`,
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// 3c. Immutable tool codes: certain tool codes must not be renamed
|
|
359
|
-
for (const file of toolFiles) {
|
|
360
|
-
const parsed = parseAuiFile(file);
|
|
361
|
-
if (!parsed?.tool)
|
|
362
|
-
continue;
|
|
363
|
-
const tool = parsed.tool;
|
|
364
|
-
const stem = path.basename(file, ".aui.json").toUpperCase();
|
|
365
|
-
if (IMMUTABLE_TOOL_CODES.includes(stem) && tool.code !== stem) {
|
|
366
|
-
issues.push({
|
|
367
|
-
type: "error",
|
|
368
|
-
file: path.relative(projectRoot, file),
|
|
369
|
-
message: `Tool code "${tool.code}" was renamed from "${stem}" — this code is immutable and must not be changed`,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
// ── 0. Duplicate-code rejection ─────────────────────────────────────────
|
|
374
|
-
//
|
|
375
|
-
// Each entity-array file (parameters, entities, integrations, rules) keys
|
|
376
|
-
// its items by `code` (or `name` for entities). The agent-settings backend
|
|
377
|
-
// treats each code as a unique platform-side ID. If the local file has two
|
|
378
|
-
// items with the same code, the push layer's git-diff machinery synthesises
|
|
379
|
-
// a `code[N]` suffix to disambiguate, and `_executePushTask` then issues
|
|
380
|
-
// PATCH/POST against `parameters/view/code[1]` — which 404s because that
|
|
381
|
-
// bracket-suffixed code doesn't exist on the platform. The user sees an
|
|
382
|
-
// opaque "PATCH param user-id[1] failed: 404" with no clue why.
|
|
383
|
-
//
|
|
384
|
-
// Catching this at validate-time turns the opaque runtime failure into a
|
|
385
|
-
// precise, actionable pre-push error pointing at the exact duplicate.
|
|
386
|
-
// No data corruption risk if missed (push fails loudly, exit non-zero,
|
|
387
|
-
// baseline correctly held back per Finding B), but the UX is significantly
|
|
388
|
-
// better when surfaced here.
|
|
389
|
-
const findDuplicates = (items, keyOf) => {
|
|
390
|
-
const counts = new Map();
|
|
391
|
-
for (const item of items ?? []) {
|
|
392
|
-
const k = keyOf(item);
|
|
393
|
-
if (!k)
|
|
394
|
-
continue;
|
|
395
|
-
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
396
|
-
}
|
|
397
|
-
// Keep only keys that appear more than once.
|
|
398
|
-
for (const [k, n] of [...counts.entries()]) {
|
|
399
|
-
if (n < 2)
|
|
400
|
-
counts.delete(k);
|
|
401
|
-
}
|
|
402
|
-
return counts;
|
|
403
|
-
};
|
|
404
|
-
const dupParams = findDuplicates(agentData.parameters, (p) => p.code);
|
|
405
|
-
for (const [code, n] of dupParams) {
|
|
406
|
-
issues.push({
|
|
407
|
-
type: "error",
|
|
408
|
-
file: "parameters.aui.json",
|
|
409
|
-
message: `Duplicate parameter code "${code}" appears ${n} times — codes must be unique within a file. The push layer can't disambiguate duplicates and would fail with an opaque 404.`,
|
|
331
|
+
// ── Resolve agent + auth context (required for remote validation) ──
|
|
332
|
+
const projectConfig = loadProjectConfig(projectRoot);
|
|
333
|
+
const session = loadSession();
|
|
334
|
+
const config = getConfig(projectRoot);
|
|
335
|
+
const agentId = projectConfig?.agent_id || session?.network_id;
|
|
336
|
+
const accountId = projectConfig?.account_id || config.accountId;
|
|
337
|
+
const organizationId = projectConfig?.organization_id || config.organizationId;
|
|
338
|
+
const authToken = config.authToken;
|
|
339
|
+
// Attach agent-scoped identifiers to the parent span as early as
|
|
340
|
+
// possible — even if validation fails further down, Logfire shows the
|
|
341
|
+
// right "which agent/account/org" row.
|
|
342
|
+
await setAgentContext(parentSpan, {
|
|
343
|
+
agentId: agentId ?? undefined,
|
|
344
|
+
agentCode: projectConfig?.agent_code ?? undefined,
|
|
345
|
+
agentManagementId: projectConfig?.agent_management_id ?? undefined,
|
|
346
|
+
versionId: projectConfig?.version_id ?? undefined,
|
|
347
|
+
versionLabel: projectConfig?.version_label ?? undefined,
|
|
348
|
+
networkId: agentId ?? undefined,
|
|
349
|
+
accountId: accountId ?? undefined,
|
|
350
|
+
organizationId: organizationId ?? undefined,
|
|
351
|
+
networkCategoryId: projectConfig?.network_category_id ?? undefined,
|
|
352
|
+
});
|
|
353
|
+
parentSpan.setAttribute("validate.has_project_config", !!projectConfig);
|
|
354
|
+
parentSpan.setAttribute("validate.has_session", !!session);
|
|
355
|
+
parentSpan.setAttribute("validate.environment", config.environment);
|
|
356
|
+
// ── Build the validate payload from local files ──
|
|
357
|
+
const { payload, index, parseErrors } = await buildValidatePayload(projectRoot);
|
|
358
|
+
parentSpan.setAttribute("validate.payload.general_settings", !!payload.general_settings);
|
|
359
|
+
parentSpan.setAttribute("validate.payload.parameters_count", payload.parameters?.length ?? 0);
|
|
360
|
+
parentSpan.setAttribute("validate.payload.entities_count", payload.entities?.length ?? 0);
|
|
361
|
+
parentSpan.setAttribute("validate.payload.rules_count", payload.rules?.length ?? 0);
|
|
362
|
+
parentSpan.setAttribute("validate.payload.integrations_count", payload.integrations?.length ?? 0);
|
|
363
|
+
parentSpan.setAttribute("validate.payload.tools_count", payload.tools?.length ?? 0);
|
|
364
|
+
parentSpan.setAttribute("validate.total_files", index.totalFiles);
|
|
365
|
+
parentSpan.setAttribute("validate.parse_errors", parseErrors.length);
|
|
366
|
+
// ── Early-exit: nothing to validate ──
|
|
367
|
+
// An empty directory (or one without any `.aui.json` files) is a no-op
|
|
368
|
+
// for validate — match the pre-remote-validate behaviour where running
|
|
369
|
+
// `aui validate` somewhere with nothing to check just printed a clean
|
|
370
|
+
// "no files" summary and exited 0. Without this guard the remote
|
|
371
|
+
// pre-check below would error out (no agent_id / no auth) and break
|
|
372
|
+
// the long-standing UX for the empty-dir smoke test in particular.
|
|
373
|
+
if (index.totalFiles === 0 &&
|
|
374
|
+
parseErrors.length === 0 &&
|
|
375
|
+
!payload.general_settings &&
|
|
376
|
+
!payload.parameters &&
|
|
377
|
+
!payload.entities &&
|
|
378
|
+
!payload.rules &&
|
|
379
|
+
!payload.integrations &&
|
|
380
|
+
!payload.tools) {
|
|
381
|
+
parentSpan.addEvent("validate.no_files_to_validate");
|
|
382
|
+
return renderAndReturn({
|
|
383
|
+
projectRoot,
|
|
384
|
+
issues: [],
|
|
385
|
+
validatedFiles: [],
|
|
386
|
+
totalFiles: 0,
|
|
387
|
+
verbose: options.verbose ?? false,
|
|
388
|
+
remoteOk: true,
|
|
410
389
|
});
|
|
411
390
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
391
|
+
// ── Decide if we can call the remote validator ──
|
|
392
|
+
// A remote call needs (1) an agent id to address the right validate
|
|
393
|
+
// endpoint, (2) an auth token, and (3) account+org for the headers
|
|
394
|
+
// the agent-settings gateway expects. Anything missing → we surface a
|
|
395
|
+
// clear error rather than silently passing.
|
|
396
|
+
const missing = [];
|
|
397
|
+
if (!agentId)
|
|
398
|
+
missing.push("agent_id (.auirc or session)");
|
|
399
|
+
if (!authToken)
|
|
400
|
+
missing.push("auth token (run `aui login`)");
|
|
401
|
+
if (!accountId)
|
|
402
|
+
missing.push("account_id");
|
|
403
|
+
if (!organizationId)
|
|
404
|
+
missing.push("organization_id");
|
|
405
|
+
// If we have JSON-syntax parse errors there is no point calling the
|
|
406
|
+
// remote validator — it can't even read the payload. Render those
|
|
407
|
+
// and exit early with `valid = false`.
|
|
408
|
+
if (parseErrors.length > 0) {
|
|
409
|
+
parentSpan.addEvent("validate.skipped_remote_due_to_parse_errors", {
|
|
410
|
+
count: parseErrors.length,
|
|
411
|
+
});
|
|
412
|
+
return renderAndReturn({
|
|
413
|
+
projectRoot,
|
|
414
|
+
issues: parseErrors,
|
|
415
|
+
validatedFiles: index.validatedFiles,
|
|
416
|
+
totalFiles: index.totalFiles,
|
|
417
|
+
verbose: options.verbose ?? false,
|
|
418
|
+
remoteOk: false,
|
|
418
419
|
});
|
|
419
420
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
if (missing.length > 0) {
|
|
422
|
+
parentSpan.addEvent("validate.missing_remote_context", {
|
|
423
|
+
missing: missing.join(", "),
|
|
424
|
+
});
|
|
425
|
+
const issue = {
|
|
423
426
|
type: "error",
|
|
424
|
-
file: "
|
|
425
|
-
message: `
|
|
427
|
+
file: ".auirc",
|
|
428
|
+
message: `Cannot run remote validation — missing: ${missing.join(", ")}. ` +
|
|
429
|
+
"Run `aui import-agent` to link an agent and `aui login` to authenticate.",
|
|
430
|
+
};
|
|
431
|
+
return renderAndReturn({
|
|
432
|
+
projectRoot,
|
|
433
|
+
issues: [issue],
|
|
434
|
+
validatedFiles: index.validatedFiles,
|
|
435
|
+
totalFiles: index.totalFiles,
|
|
436
|
+
verbose: options.verbose ?? false,
|
|
437
|
+
remoteOk: false,
|
|
426
438
|
});
|
|
427
439
|
}
|
|
428
|
-
//
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const entityNames = new Set(agentData.entities?.map((e) => e.name) || []);
|
|
466
|
-
const toolCodes = new Set(agentData.tools?.map((t) => t.code) || []);
|
|
467
|
-
// ── 1. Tool → Integration references ──
|
|
468
|
-
for (const tool of agentData.tools || []) {
|
|
469
|
-
for (const ref of tool.integrations || []) {
|
|
470
|
-
if (!integrationCodes.has(ref.code)) {
|
|
471
|
-
issues.push({
|
|
472
|
-
type: "error",
|
|
473
|
-
file: `tools/${tool.code}.aui.json`,
|
|
474
|
-
message: `Tool "${tool.code}" references integration "${ref.code}" which is not defined in integrations`,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// ── 2. Tool → Parameter references (required/optional) ──
|
|
480
|
-
for (const tool of agentData.tools || []) {
|
|
481
|
-
if (tool.config?.params) {
|
|
482
|
-
for (const req of tool.config.params.required || []) {
|
|
483
|
-
const codes = Array.isArray(req) ? req : [req];
|
|
484
|
-
for (const code of codes) {
|
|
485
|
-
if (!paramCodes.has(code)) {
|
|
486
|
-
issues.push({
|
|
487
|
-
type: "error",
|
|
488
|
-
file: `tools/${tool.code}.aui.json`,
|
|
489
|
-
message: `Tool "${tool.code}" requires parameter "${code}" which is not defined in parameters`,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
for (const code of tool.config.params.optional || []) {
|
|
495
|
-
if (!paramCodes.has(code)) {
|
|
496
|
-
issues.push({
|
|
497
|
-
type: "error",
|
|
498
|
-
file: `tools/${tool.code}.aui.json`,
|
|
499
|
-
message: `Tool "${tool.code}" references optional parameter "${code}" which is not defined in parameters`,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
// ── 3. Tool → Entity references (available_context) ──
|
|
506
|
-
for (const tool of agentData.tools || []) {
|
|
507
|
-
if (tool.available_context?.entities) {
|
|
508
|
-
for (const entityName of tool.available_context.entities) {
|
|
509
|
-
if (!entityNames.has(entityName)) {
|
|
510
|
-
issues.push({
|
|
511
|
-
type: "error",
|
|
512
|
-
file: `tools/${tool.code}.aui.json`,
|
|
513
|
-
message: `Tool "${tool.code}" references entity "${entityName}" in available_context which is not defined in entities`,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// ── 4. Tool rules → Parameter references (pre/summary then.params) ──
|
|
520
|
-
for (const tool of agentData.tools || []) {
|
|
521
|
-
for (const section of ["pre", "summary"]) {
|
|
522
|
-
const ruleSection = tool.rules?.[section];
|
|
523
|
-
if (!ruleSection)
|
|
524
|
-
continue;
|
|
525
|
-
for (const [category, ruleArray] of Object.entries(ruleSection)) {
|
|
526
|
-
if (!Array.isArray(ruleArray))
|
|
527
|
-
continue;
|
|
528
|
-
for (const rule of ruleArray) {
|
|
529
|
-
const r = rule;
|
|
530
|
-
if (r.then?.params?.mandatory) {
|
|
531
|
-
for (const p of r.then.params.mandatory) {
|
|
532
|
-
if (!paramCodes.has(p)) {
|
|
533
|
-
issues.push({
|
|
534
|
-
type: "error",
|
|
535
|
-
file: `tools/${tool.code}.aui.json`,
|
|
536
|
-
message: `Tool "${tool.code}" rule ${section}.${category} references mandatory parameter "${p}" which is not defined in parameters`,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (r.then?.params?.optional) {
|
|
542
|
-
for (const p of r.then.params.optional) {
|
|
543
|
-
if (!paramCodes.has(p)) {
|
|
544
|
-
issues.push({
|
|
545
|
-
type: "error",
|
|
546
|
-
file: `tools/${tool.code}.aui.json`,
|
|
547
|
-
message: `Tool "${tool.code}" rule ${section}.${category} references optional parameter "${p}" which is not defined in parameters`,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
// ── 5. Global rules → Tool references (applies_to) ──
|
|
557
|
-
for (const rule of rulesList) {
|
|
558
|
-
const appliesTo = rule.applies_to;
|
|
559
|
-
if (appliesTo) {
|
|
560
|
-
for (const toolCode of appliesTo) {
|
|
561
|
-
if (!toolCodes.has(toolCode)) {
|
|
562
|
-
issues.push({
|
|
563
|
-
type: "error",
|
|
564
|
-
file: "rules.aui.json",
|
|
565
|
-
message: `Rule "${rule.code}" applies_to references tool "${toolCode}" which is not defined in tools`,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// ── 6. Global rules → Parameter references (conditions + then.params) ──
|
|
572
|
-
for (const rule of rulesList) {
|
|
573
|
-
const ruleCode = (rule.code || rule.description || "unknown");
|
|
574
|
-
const conditions = [];
|
|
575
|
-
const when = rule.when;
|
|
576
|
-
if (when) {
|
|
577
|
-
for (const key of ["all", "any", "not"]) {
|
|
578
|
-
const items = when[key];
|
|
579
|
-
if (Array.isArray(items)) {
|
|
580
|
-
collectConditions(items, conditions);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
for (const cond of conditions) {
|
|
585
|
-
const condParams = cond.params;
|
|
586
|
-
if (condParams) {
|
|
587
|
-
for (const p of condParams) {
|
|
588
|
-
if (!paramCodes.has(p)) {
|
|
589
|
-
issues.push({
|
|
590
|
-
type: "error",
|
|
591
|
-
file: "rules.aui.json",
|
|
592
|
-
message: `Rule "${ruleCode}" condition references parameter "${p}" which is not defined in parameters`,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
const then = rule.then;
|
|
599
|
-
if (then?.params?.mandatory) {
|
|
600
|
-
for (const p of then.params.mandatory) {
|
|
601
|
-
if (!paramCodes.has(p)) {
|
|
602
|
-
issues.push({
|
|
603
|
-
type: "error",
|
|
604
|
-
file: "rules.aui.json",
|
|
605
|
-
message: `Rule "${ruleCode}" then.params.mandatory references parameter "${p}" which is not defined in parameters`,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
if (then?.params?.optional) {
|
|
611
|
-
for (const p of then.params.optional) {
|
|
612
|
-
if (!paramCodes.has(p)) {
|
|
613
|
-
issues.push({
|
|
614
|
-
type: "error",
|
|
615
|
-
file: "rules.aui.json",
|
|
616
|
-
message: `Rule "${ruleCode}" then.params.optional references parameter "${p}" which is not defined in parameters`,
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// ── Existing validations ──
|
|
623
|
-
const responseMappingParams = new Set();
|
|
624
|
-
const requestSchemaParams = new Set();
|
|
625
|
-
for (const integration of agentData.integrations || []) {
|
|
626
|
-
const settings = integration.settings;
|
|
627
|
-
if (!settings)
|
|
628
|
-
continue;
|
|
629
|
-
if (settings.request_schema) {
|
|
630
|
-
for (const field of settings.request_schema) {
|
|
631
|
-
for (const p of field.params || []) {
|
|
632
|
-
requestSchemaParams.add(p);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (settings.response_mapping) {
|
|
637
|
-
for (const groups of Object.values(settings.response_mapping)) {
|
|
638
|
-
for (const group of groups) {
|
|
639
|
-
for (const mapping of group.mappings || []) {
|
|
640
|
-
if (mapping.param) {
|
|
641
|
-
responseMappingParams.add(mapping.param);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
// Validate integration code matches kebab-case transformation of name
|
|
649
|
-
for (const integration of agentData.integrations || []) {
|
|
650
|
-
if (integration.name) {
|
|
651
|
-
const expectedCode = integration.name
|
|
652
|
-
.toLowerCase()
|
|
653
|
-
.replace(/[\s_]+/g, "-");
|
|
654
|
-
if (integration.code !== expectedCode) {
|
|
655
|
-
issues.push({
|
|
656
|
-
type: "error",
|
|
657
|
-
file: "integrations.aui.json",
|
|
658
|
-
message: `Integration "${integration.name}" has code "${integration.code}" but expected "${expectedCode}" (name lowercased with spaces and underscores replaced by dashes)`,
|
|
659
|
-
});
|
|
440
|
+
// ── Call remote validation ──
|
|
441
|
+
const client = new AUIClient({
|
|
442
|
+
baseUrl: config.apiUrl,
|
|
443
|
+
authToken: authToken,
|
|
444
|
+
accountId: accountId,
|
|
445
|
+
organizationId: organizationId,
|
|
446
|
+
environment: config.environment,
|
|
447
|
+
});
|
|
448
|
+
const tracer = getTracer();
|
|
449
|
+
let response = null;
|
|
450
|
+
let remoteError = null;
|
|
451
|
+
await tracer.startActiveSpan("aui.validate.remote", async (rSpan) => {
|
|
452
|
+
rSpan.setAttribute("validate.agent_id", agentId);
|
|
453
|
+
rSpan.setAttribute("validate.account_id", accountId);
|
|
454
|
+
rSpan.setAttribute("validate.organization_id", organizationId);
|
|
455
|
+
rSpan.setAttribute("validate.environment", config.environment);
|
|
456
|
+
rSpan.setAttribute("validate.payload.tools_count", payload.tools?.length ?? 0);
|
|
457
|
+
rSpan.setAttribute("validate.payload.parameters_count", payload.parameters?.length ?? 0);
|
|
458
|
+
rSpan.setAttribute("validate.payload.entities_count", payload.entities?.length ?? 0);
|
|
459
|
+
const startedAt = Date.now();
|
|
460
|
+
try {
|
|
461
|
+
response = await client.agentManagement.validateAgent(agentId, payload);
|
|
462
|
+
rSpan.setAttribute("validate.remote.duration_ms", Date.now() - startedAt);
|
|
463
|
+
rSpan.setAttribute("validate.remote.valid", response.valid);
|
|
464
|
+
const errorCount = countIssues(response, "errors");
|
|
465
|
+
const warningCount = countIssues(response, "warnings");
|
|
466
|
+
rSpan.setAttribute("validate.remote.error_count", errorCount);
|
|
467
|
+
rSpan.setAttribute("validate.remote.warning_count", warningCount);
|
|
468
|
+
rSpan.setStatus({ code: SpanStatusCode.OK });
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
+
remoteError = msg;
|
|
473
|
+
rSpan.setAttribute("validate.remote.duration_ms", Date.now() - startedAt);
|
|
474
|
+
if (err instanceof AUIAPIError) {
|
|
475
|
+
rSpan.setAttribute("http.status_code", err.status);
|
|
476
|
+
rSpan.setAttribute("validate.remote.error_body", typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null));
|
|
660
477
|
}
|
|
478
|
+
rSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg });
|
|
479
|
+
rSpan.recordException(err instanceof Error ? err : new Error(msg));
|
|
661
480
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
for (const integration of agentData.integrations || []) {
|
|
665
|
-
if (integration.type !== "API" || integration.code === "web-search")
|
|
666
|
-
continue;
|
|
667
|
-
const settings = integration.settings;
|
|
668
|
-
if (!settings)
|
|
669
|
-
continue;
|
|
670
|
-
const mapping = settings.response_mapping;
|
|
671
|
-
const hasMapping = mapping &&
|
|
672
|
-
typeof mapping === "object" &&
|
|
673
|
-
Object.values(mapping).some((groups) => Array.isArray(groups) &&
|
|
674
|
-
groups.some((g) => Array.isArray(g.mappings) && g.mappings.length > 0));
|
|
675
|
-
if (!hasMapping) {
|
|
676
|
-
issues.push({
|
|
677
|
-
type: "error",
|
|
678
|
-
file: "integrations.aui.json",
|
|
679
|
-
message: `Integration "${integration.code}" has empty response mappings — API integrations must have at least one response mapping`,
|
|
680
|
-
});
|
|
481
|
+
finally {
|
|
482
|
+
rSpan.end();
|
|
681
483
|
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
484
|
+
});
|
|
485
|
+
if (!response) {
|
|
486
|
+
// Remote validation could not be performed. We deliberately treat
|
|
487
|
+
// this as a hard failure (return false) rather than silently passing
|
|
488
|
+
// — letting `aui push` proceed against an un-validated bundle would
|
|
489
|
+
// re-introduce exactly the class of bugs the duplicate-code rejection
|
|
490
|
+
// and other validate-time checks were added to prevent.
|
|
491
|
+
parentSpan.setAttribute("validate.remote.failure", remoteError ?? "unknown");
|
|
492
|
+
const friendly = formatRemoteFailure(remoteError);
|
|
493
|
+
const issue = {
|
|
494
|
+
type: "error",
|
|
495
|
+
file: "(remote validate)",
|
|
496
|
+
message: friendly,
|
|
497
|
+
};
|
|
498
|
+
return renderAndReturn({
|
|
499
|
+
projectRoot,
|
|
500
|
+
issues: [issue],
|
|
501
|
+
validatedFiles: index.validatedFiles,
|
|
502
|
+
totalFiles: index.totalFiles,
|
|
503
|
+
verbose: options.verbose ?? false,
|
|
504
|
+
remoteOk: false,
|
|
693
505
|
});
|
|
694
|
-
const committedData = JSON.parse(committedRaw);
|
|
695
|
-
for (const int of committedData.integrations || []) {
|
|
696
|
-
committedIntegrations.set(int.code, JSON.stringify(int));
|
|
697
|
-
}
|
|
698
506
|
}
|
|
699
|
-
|
|
700
|
-
|
|
507
|
+
const responseLocal = response;
|
|
508
|
+
const remoteIssues = mapResponseToIssues(responseLocal, index);
|
|
509
|
+
const allIssues = [...parseErrors, ...remoteIssues];
|
|
510
|
+
parentSpan.setAttribute("validate.result.error_count", allIssues.filter((i) => i.type === "error").length);
|
|
511
|
+
parentSpan.setAttribute("validate.result.warning_count", allIssues.filter((i) => i.type === "warning").length);
|
|
512
|
+
return renderAndReturn({
|
|
513
|
+
projectRoot,
|
|
514
|
+
issues: allIssues,
|
|
515
|
+
validatedFiles: index.validatedFiles,
|
|
516
|
+
totalFiles: index.totalFiles,
|
|
517
|
+
verbose: options.verbose ?? false,
|
|
518
|
+
remoteOk: responseLocal.valid && allIssues.every((i) => i.type !== "error"),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/** Count issues of a given kind across all sections in the response. */
|
|
522
|
+
function countIssues(response, kind) {
|
|
523
|
+
let count = 0;
|
|
524
|
+
const gs = response.general_settings;
|
|
525
|
+
if (gs)
|
|
526
|
+
count += (gs[kind] ?? []).length;
|
|
527
|
+
const arr = (section) => {
|
|
528
|
+
if (!section)
|
|
529
|
+
return 0;
|
|
530
|
+
return section.reduce((s, it) => s + (it.result[kind] ?? []).length, 0);
|
|
531
|
+
};
|
|
532
|
+
count += arr(response.parameters);
|
|
533
|
+
count += arr(response.entities);
|
|
534
|
+
count += arr(response.rules);
|
|
535
|
+
count += arr(response.integrations);
|
|
536
|
+
count += arr(response.agent_tools);
|
|
537
|
+
return count;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Build a user-facing message for a remote-validation infra failure so
|
|
541
|
+
* `aui push` (and a direct `aui validate` call) tells the user exactly
|
|
542
|
+
* which knob to turn — instead of a raw `validate agent: 502 ...` blob.
|
|
543
|
+
*/
|
|
544
|
+
function formatRemoteFailure(err) {
|
|
545
|
+
const base = err ?? "remote validation failed for an unknown reason";
|
|
546
|
+
// The error strings from AUIAPIError always start with "API Error <code>:"
|
|
547
|
+
// and writeV2-style errors say "<label> failed: <status> ...". Anchor
|
|
548
|
+
// status-code matching to those prefixes so an agent_id substring like
|
|
549
|
+
// "593" in the URL never gets misclassified as a 5xx response.
|
|
550
|
+
const statusMatch = base.match(/API Error (\d{3})/) || base.match(/failed: (\d{3})/);
|
|
551
|
+
const status = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
552
|
+
if (/timed out/i.test(base) || /timeout/i.test(base)) {
|
|
553
|
+
return `Remote validation timed out: ${base}. Retry, or use --force on \`aui push\` to bypass validation temporarily.`;
|
|
701
554
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
continue;
|
|
705
|
-
const settings = integration.settings;
|
|
706
|
-
if (!settings)
|
|
707
|
-
continue;
|
|
708
|
-
// Skip unchanged integrations
|
|
709
|
-
const currentStr = JSON.stringify(integration);
|
|
710
|
-
const committedStr = committedIntegrations.get(integration.code);
|
|
711
|
-
if (committedStr === currentStr)
|
|
712
|
-
continue;
|
|
713
|
-
// test_curl must not be nullish
|
|
714
|
-
if (!settings.test_curl) {
|
|
715
|
-
issues.push({
|
|
716
|
-
type: curlIssueType,
|
|
717
|
-
file: "integrations.aui.json",
|
|
718
|
-
message: `Integration "${integration.code}" is missing test_curl — API integrations must have a test_curl command`,
|
|
719
|
-
});
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
// test_curl must be a valid curl command
|
|
723
|
-
const trimmedCurl = settings.test_curl.trim();
|
|
724
|
-
if (!trimmedCurl.startsWith("curl ") && trimmedCurl !== "curl") {
|
|
725
|
-
issues.push({
|
|
726
|
-
type: curlIssueType,
|
|
727
|
-
file: "integrations.aui.json",
|
|
728
|
-
message: `Integration "${integration.code}" test_curl must be a valid curl command (must start with "curl")`,
|
|
729
|
-
});
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
// test_curl must include the endpoint URL
|
|
733
|
-
if (settings.endpoint?.url && !trimmedCurl.includes(settings.endpoint.url)) {
|
|
734
|
-
issues.push({
|
|
735
|
-
type: curlIssueType,
|
|
736
|
-
file: "integrations.aui.json",
|
|
737
|
-
message: `Integration "${integration.code}" test_curl does not include the endpoint URL "${settings.endpoint.url}" — the test_curl should use the same URL as the endpoint`,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
// Execute the test_curl command
|
|
741
|
-
let curlOutput;
|
|
742
|
-
try {
|
|
743
|
-
curlOutput = execSync(settings.test_curl, {
|
|
744
|
-
encoding: "utf-8",
|
|
745
|
-
timeout: 30_000,
|
|
746
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
747
|
-
}).trim();
|
|
748
|
-
}
|
|
749
|
-
catch (e) {
|
|
750
|
-
issues.push({
|
|
751
|
-
type: curlIssueType,
|
|
752
|
-
file: "integrations.aui.json",
|
|
753
|
-
message: `Integration "${integration.code}" test_curl failed to execute: ${e instanceof Error ? e.message : e}`,
|
|
754
|
-
});
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
// Parse the curl response
|
|
758
|
-
let curlResponse;
|
|
759
|
-
try {
|
|
760
|
-
curlResponse = JSON.parse(curlOutput);
|
|
761
|
-
}
|
|
762
|
-
catch {
|
|
763
|
-
issues.push({
|
|
764
|
-
type: curlIssueType,
|
|
765
|
-
file: "integrations.aui.json",
|
|
766
|
-
message: `Integration "${integration.code}" test_curl response is not valid JSON`,
|
|
767
|
-
});
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
// test_curl_response must exist
|
|
771
|
-
if (settings.test_curl_response === undefined || settings.test_curl_response === null) {
|
|
772
|
-
issues.push({
|
|
773
|
-
type: curlIssueType,
|
|
774
|
-
file: "integrations.aui.json",
|
|
775
|
-
message: `Integration "${integration.code}" is missing test_curl_response — API integrations must have a test_curl_response to validate against`,
|
|
776
|
-
});
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
// Parse test_curl_response if it's a string
|
|
780
|
-
let testCurlResponse = settings.test_curl_response;
|
|
781
|
-
if (typeof testCurlResponse === "string") {
|
|
782
|
-
try {
|
|
783
|
-
testCurlResponse = JSON.parse(testCurlResponse);
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
issues.push({
|
|
787
|
-
type: curlIssueType,
|
|
788
|
-
file: "integrations.aui.json",
|
|
789
|
-
message: `Integration "${integration.code}" test_curl_response is a string but not valid JSON`,
|
|
790
|
-
});
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
// Compare test_curl_response schema (keys) and values separately
|
|
795
|
-
const collectKeysDeep = (obj, prefix = "") => {
|
|
796
|
-
const keys = new Set();
|
|
797
|
-
if (Array.isArray(obj)) {
|
|
798
|
-
keys.add(prefix + "[]");
|
|
799
|
-
if (obj.length > 0) {
|
|
800
|
-
for (const k of collectKeysDeep(obj[0], prefix + "[]."))
|
|
801
|
-
keys.add(k);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
else if (obj && typeof obj === "object") {
|
|
805
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
806
|
-
const fullKey = prefix + k;
|
|
807
|
-
keys.add(fullKey);
|
|
808
|
-
if (v && typeof v === "object") {
|
|
809
|
-
for (const nested of collectKeysDeep(v, fullKey + "."))
|
|
810
|
-
keys.add(nested);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return keys;
|
|
815
|
-
};
|
|
816
|
-
const expectedKeys = collectKeysDeep(testCurlResponse);
|
|
817
|
-
const actualKeys = collectKeysDeep(curlResponse);
|
|
818
|
-
const missingKeys = [...expectedKeys].filter((k) => !actualKeys.has(k));
|
|
819
|
-
const extraKeys = [...actualKeys].filter((k) => !expectedKeys.has(k));
|
|
820
|
-
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
|
821
|
-
// Keys/schema are different — error
|
|
822
|
-
const parts = [];
|
|
823
|
-
if (missingKeys.length > 0)
|
|
824
|
-
parts.push(`missing keys: ${missingKeys.join(", ")}`);
|
|
825
|
-
if (extraKeys.length > 0)
|
|
826
|
-
parts.push(`unexpected keys: ${extraKeys.join(", ")}`);
|
|
827
|
-
issues.push({
|
|
828
|
-
type: curlIssueType,
|
|
829
|
-
file: "integrations.aui.json",
|
|
830
|
-
message: `Integration "${integration.code}" test_curl response schema differs from test_curl_response — ${parts.join("; ")}`,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
// Keys match but values might differ — warning
|
|
835
|
-
const expectedStr = JSON.stringify(testCurlResponse);
|
|
836
|
-
const actualStr = JSON.stringify(curlResponse);
|
|
837
|
-
if (expectedStr !== actualStr) {
|
|
838
|
-
issues.push({
|
|
839
|
-
type: "warning",
|
|
840
|
-
file: "integrations.aui.json",
|
|
841
|
-
message: `Integration "${integration.code}" test_curl response values differ from test_curl_response (schema matches but values changed)`,
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
// Cross-reference: mapping keys must be actual keys in the curl response
|
|
846
|
-
if (settings.response_mapping && curlResponse && typeof curlResponse === "object") {
|
|
847
|
-
// Collect all keys from the curl response (recursively from first item if array)
|
|
848
|
-
const collectKeys = (obj) => {
|
|
849
|
-
const keys = new Set();
|
|
850
|
-
if (Array.isArray(obj)) {
|
|
851
|
-
if (obj.length > 0) {
|
|
852
|
-
for (const k of collectKeys(obj[0]))
|
|
853
|
-
keys.add(k);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
else if (obj && typeof obj === "object") {
|
|
857
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
858
|
-
keys.add(k);
|
|
859
|
-
if (v && typeof v === "object") {
|
|
860
|
-
for (const nested of collectKeys(v))
|
|
861
|
-
keys.add(nested);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
return keys;
|
|
866
|
-
};
|
|
867
|
-
const responseKeys = collectKeys(curlResponse);
|
|
868
|
-
for (const groups of Object.values(settings.response_mapping)) {
|
|
869
|
-
for (const group of groups) {
|
|
870
|
-
for (const mapping of group.mappings || []) {
|
|
871
|
-
const mappingKeys = Array.isArray(mapping.key) ? mapping.key : [mapping.key];
|
|
872
|
-
for (const k of mappingKeys) {
|
|
873
|
-
if (!responseKeys.has(k)) {
|
|
874
|
-
issues.push({
|
|
875
|
-
type: curlIssueType,
|
|
876
|
-
file: "integrations.aui.json",
|
|
877
|
-
message: `Integration "${integration.code}" response mapping key "${k}" (param: "${mapping.param}") is not found in the actual curl response`,
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
555
|
+
if (/econnrefused|enotfound|network/i.test(base)) {
|
|
556
|
+
return `Remote validation could not reach the backend: ${base}. Check connectivity; use --force on \`aui push\` if urgent.`;
|
|
885
557
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
for (const p of entity.parameters || []) {
|
|
889
|
-
entityParams.add(p);
|
|
890
|
-
}
|
|
558
|
+
if (status === 401 || /unauthor/i.test(base)) {
|
|
559
|
+
return `Remote validation rejected the request (auth): ${base}. Run \`aui login\` and try again.`;
|
|
891
560
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
issues.push({
|
|
895
|
-
type: "error",
|
|
896
|
-
file: "integrations.aui.json",
|
|
897
|
-
message: `Integration response mapping references parameter "${p}" which is not defined in parameters`,
|
|
898
|
-
});
|
|
899
|
-
}
|
|
561
|
+
if (status === 404) {
|
|
562
|
+
return `Remote validation endpoint returned 404: ${base}. Verify .auirc.agent_id matches an existing agent.`;
|
|
900
563
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
issues.push({
|
|
904
|
-
type: "error",
|
|
905
|
-
file: "integrations.aui.json",
|
|
906
|
-
message: `Integration request schema references parameter "${p}" which is not defined in parameters`,
|
|
907
|
-
});
|
|
908
|
-
}
|
|
564
|
+
if (status === 422) {
|
|
565
|
+
return `Remote validator could not parse the payload: ${base}.`;
|
|
909
566
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
issues.push({
|
|
913
|
-
type: "error",
|
|
914
|
-
file: "entities.aui.json",
|
|
915
|
-
message: `Entity references parameter "${p}" which is not defined in parameters`,
|
|
916
|
-
});
|
|
917
|
-
}
|
|
567
|
+
if (status !== null && status >= 500 && status < 600) {
|
|
568
|
+
return `Remote validation backend error (${status}): ${base}. Retry shortly; use --force on \`aui push\` if urgent.`;
|
|
918
569
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
570
|
+
return `Remote validation failed: ${base}.`;
|
|
571
|
+
}
|
|
572
|
+
function renderAndReturn(args) {
|
|
573
|
+
const { schemaIssues, crossRefIssues } = categorizeIssues(args.issues);
|
|
922
574
|
const result = {
|
|
923
|
-
projectRoot,
|
|
924
|
-
schemaCount
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
575
|
+
projectRoot: args.projectRoot,
|
|
576
|
+
// schemaCount/schemaNames are legacy fields from local AJV validation.
|
|
577
|
+
// We keep the shape stable but report `1` to mean "the remote schema",
|
|
578
|
+
// so the UI's "0 schemas / no .dschema.json files" warning never fires.
|
|
579
|
+
schemaCount: 1,
|
|
580
|
+
schemaNames: ["remote:agent-settings/v1/validate"],
|
|
581
|
+
totalFiles: args.totalFiles,
|
|
582
|
+
validatedFiles: [...new Set(args.validatedFiles)],
|
|
928
583
|
schemaIssues,
|
|
929
584
|
crossRefIssues,
|
|
930
|
-
verbose:
|
|
585
|
+
verbose: args.verbose,
|
|
931
586
|
};
|
|
587
|
+
const allErrors = args.issues.filter((i) => i.type === "error");
|
|
932
588
|
if (isJsonMode()) {
|
|
933
|
-
const allErrors = issues.filter((i) => i.type === "error");
|
|
934
589
|
outputJson({
|
|
935
|
-
valid: allErrors.length === 0,
|
|
936
|
-
project_root: projectRoot,
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
590
|
+
valid: allErrors.length === 0 && args.remoteOk,
|
|
591
|
+
project_root: args.projectRoot,
|
|
592
|
+
remote_ok: args.remoteOk,
|
|
593
|
+
schemas: result.schemaNames,
|
|
594
|
+
total_files: args.totalFiles,
|
|
595
|
+
validated_files: result.validatedFiles,
|
|
940
596
|
schema_issues: schemaIssues,
|
|
941
597
|
cross_ref_issues: crossRefIssues,
|
|
942
598
|
error_count: allErrors.length,
|
|
943
599
|
});
|
|
944
|
-
return allErrors.length === 0;
|
|
600
|
+
return allErrors.length === 0 && args.remoteOk;
|
|
945
601
|
}
|
|
946
602
|
render(_jsx(ValidateView, { data: result }));
|
|
947
|
-
|
|
948
|
-
return allErrors.length === 0;
|
|
603
|
+
return allErrors.length === 0 && args.remoteOk;
|
|
949
604
|
}
|
|
950
605
|
//# sourceMappingURL=validate.js.map
|