@avocadostudio-ai/orchestrator-core 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -21,7 +21,7 @@ import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
|
21
21
|
import { resolve, basename } from "node:path";
|
|
22
22
|
import { randomUUID } from "node:crypto";
|
|
23
23
|
import { z } from "zod";
|
|
24
|
-
import { operationSchema, blockManifestSchema, siteConfigSchema, declareBlockCatalogue, undeclaredBlockTypes, EDITOR_PROTOCOL_VERSION, withLoopbackSpellings, editorAllowHeaders, compareDraftSecret } from "@avocadostudio-ai/shared";
|
|
24
|
+
import { operationSchema, blockManifestSchema, siteConfigSchema, declareBlockCatalogue, undeclaredBlockTypes, EDITOR_PROTOCOL_VERSION, withLoopbackSpellings, editorAllowHeaders, compareDraftSecret, buildBlockManifest } from "@avocadostudio-ai/shared";
|
|
25
25
|
import { keyFileHint } from "../env-file-location.js";
|
|
26
26
|
import { chatRequestBodySchema } from "../nlp/intent-detection.js";
|
|
27
27
|
import { applyOpsAtomically, pickFocusBlockId, pickUpdatedSlug, toErrorDetail, classifyGuardrailError } from "../ops/ops-engine.js";
|
|
@@ -65,6 +65,7 @@ import { resolveCapabilities } from "../cms/adapter.js";
|
|
|
65
65
|
import { mediaSourceFromUnknown } from "../cms/media-sources.js";
|
|
66
66
|
import { extractAccessToken, isAccessGateEnabled, isValidAccessToken, mintAccessToken, verifyAccessPassword } from "../http/access-tokens.js";
|
|
67
67
|
import { editorCredentials } from "../http/editor-credentials.js";
|
|
68
|
+
import { OperationError } from "../errors.js";
|
|
68
69
|
import { clearLibraryDispatch, declareLibraryMount, observeLibraryMount, registerLibraryDispatch } from "./library-mount.js";
|
|
69
70
|
import { checkAuth, resolveAuth, CLOSED_HINT } from "./auth.js";
|
|
70
71
|
import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
|
|
@@ -1605,6 +1606,19 @@ export function createOrchestrator(config = {}) {
|
|
|
1605
1606
|
manifest = pm.data;
|
|
1606
1607
|
}
|
|
1607
1608
|
}
|
|
1609
|
+
/*
|
|
1610
|
+
* No manifest in the request: use this mount's own. The editor sends one
|
|
1611
|
+
* with every chat turn, but an MCP agent or a script calling /ops sends
|
|
1612
|
+
* none, and without it every manifest check in the ops engine is skipped:
|
|
1613
|
+
* read-only fields, fixed blocks, list discriminators, and list rows in a
|
|
1614
|
+
* shape nothing renders. Library mode is the one host that already knows
|
|
1615
|
+
* the site's blocks, the same ones `GET /blocks/manifest` serves.
|
|
1616
|
+
*/
|
|
1617
|
+
if (!manifest) {
|
|
1618
|
+
const own = buildBlockManifest();
|
|
1619
|
+
if (own.blocks.length > 0)
|
|
1620
|
+
manifest = own;
|
|
1621
|
+
}
|
|
1608
1622
|
/*
|
|
1609
1623
|
* Validate-only, before anything is snapshotted or bumped. This branch
|
|
1610
1624
|
* did not exist: `dryRun` was parsed out of the body by nobody and the
|
|
@@ -1703,7 +1717,10 @@ export function createOrchestrator(config = {}) {
|
|
|
1703
1717
|
}
|
|
1704
1718
|
catch (error) {
|
|
1705
1719
|
const reason = toErrorDetail(error);
|
|
1706
|
-
|
|
1720
|
+
// An OperationError already knows its category; the text classifier is
|
|
1721
|
+
// for errors that do not, and called a refused list row "internal_error".
|
|
1722
|
+
const category = error instanceof OperationError ? error.category : undefined;
|
|
1723
|
+
return jsonResponse({ error: reason, errorCode: category ?? classifyGuardrailError(reason) }, { status: 400, cors });
|
|
1707
1724
|
}
|
|
1708
1725
|
}
|
|
1709
1726
|
/*
|
package/dist/ops/ops-engine.js
CHANGED
|
@@ -766,6 +766,73 @@ function _requireListDiscriminator(manifestByType, blockType, listKey, item) {
|
|
|
766
766
|
const choices = admissible.length > 0 ? `: ${admissible.join(", ")}` : "";
|
|
767
767
|
throw new OperationError(`A new ${listKey} row must set "${discriminator}", which names its type${choices}`, { category: "schema_violation" });
|
|
768
768
|
}
|
|
769
|
+
/*
|
|
770
|
+
* A list row must use the list's own field names.
|
|
771
|
+
*
|
|
772
|
+
* Integrations declare item schemas loose, so bookkeeping keys a CMS adds
|
|
773
|
+
* (`_type`, `_key`) ride through validation, and every field is optional. Such
|
|
774
|
+
* a schema accepts any object. A planner that had just written Avocado's
|
|
775
|
+
* built-in FAQ shape, `{ q, a }`, into a site whose accordion reads
|
|
776
|
+
* `{ title, body }` got an "applied" and a row that rendered empty, with the
|
|
777
|
+
* text present in the draft under names nothing reads.
|
|
778
|
+
*
|
|
779
|
+
* So a key is refused when the list neither declares it nor already holds it
|
|
780
|
+
* on another row: copying an existing row, undeclared keys and all, still
|
|
781
|
+
* works, and only a name the list has never seen is refused. The error names
|
|
782
|
+
* the fields the list does use, so the planner can correct itself. Lists with
|
|
783
|
+
* no declared item fields are left alone, since there is nothing to check
|
|
784
|
+
* against.
|
|
785
|
+
*/
|
|
786
|
+
/** Property names an item of `listKey` may carry, per the block's JSON Schema. */
|
|
787
|
+
function _schemaItemKeys(definition, listKey) {
|
|
788
|
+
const props = definition.propsSchema?.properties;
|
|
789
|
+
const listSchema = props?.[listKey];
|
|
790
|
+
const keys = new Set();
|
|
791
|
+
const visit = (node) => {
|
|
792
|
+
if (!node || typeof node !== "object")
|
|
793
|
+
return;
|
|
794
|
+
const n = node;
|
|
795
|
+
for (const key of Object.keys(n.properties ?? {}))
|
|
796
|
+
keys.add(key);
|
|
797
|
+
for (const branch of [...(n.oneOf ?? []), ...(n.anyOf ?? []), ...(n.allOf ?? [])])
|
|
798
|
+
visit(branch);
|
|
799
|
+
};
|
|
800
|
+
visit(listSchema?.items);
|
|
801
|
+
return [...keys];
|
|
802
|
+
}
|
|
803
|
+
function _requireKnownItemKeys(manifestByType, blockType, listKey, list, item) {
|
|
804
|
+
const definition = manifestByType.get(blockType);
|
|
805
|
+
if (!definition)
|
|
806
|
+
return;
|
|
807
|
+
const listField = resolveManifestFieldMeta(definition).listFields[listKey];
|
|
808
|
+
if (!listField)
|
|
809
|
+
return;
|
|
810
|
+
const declared = new Set(Object.keys(listField.itemFields ?? {}));
|
|
811
|
+
for (const fields of Object.values(listField.itemFieldsByType ?? {})) {
|
|
812
|
+
for (const key of Object.keys(fields ?? {}))
|
|
813
|
+
declared.add(key);
|
|
814
|
+
}
|
|
815
|
+
if (listField.discriminator)
|
|
816
|
+
declared.add(listField.discriminator);
|
|
817
|
+
// The props schema names every item property, optional ones included; the
|
|
818
|
+
// field meta above names only what the panel offers.
|
|
819
|
+
for (const key of _schemaItemKeys(definition, listKey))
|
|
820
|
+
declared.add(key);
|
|
821
|
+
if (declared.size === 0)
|
|
822
|
+
return;
|
|
823
|
+
const known = new Set(declared);
|
|
824
|
+
for (const row of list) {
|
|
825
|
+
if (row && typeof row === "object" && !Array.isArray(row)) {
|
|
826
|
+
for (const key of Object.keys(row))
|
|
827
|
+
known.add(key);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const unknown = Object.keys(item).filter((key) => key !== "id" && !key.startsWith("_") && !known.has(key));
|
|
831
|
+
if (unknown.length === 0)
|
|
832
|
+
return;
|
|
833
|
+
throw new OperationError(`${listKey} rows have no field ${unknown.map((k) => `"${k}"`).join(", ")}. ` +
|
|
834
|
+
`Use the list's own fields: ${[...declared].filter((key) => !key.startsWith("_")).join(", ")}`, { category: "schema_violation" });
|
|
835
|
+
}
|
|
769
836
|
function _requireManifestComponent(manifestByType, blockType, operationName) {
|
|
770
837
|
if (manifestByType.size === 0)
|
|
771
838
|
return;
|
|
@@ -1449,6 +1516,7 @@ async function _applyOpsAtomicallyUnsafe(session, ops, options) {
|
|
|
1449
1516
|
// silently resolve to the first occurrence.
|
|
1450
1517
|
const newItem = structuredClone(op.item);
|
|
1451
1518
|
_requireListDiscriminator(manifestByType, block.type, op.listKey, newItem);
|
|
1519
|
+
_requireKnownItemKeys(manifestByType, block.type, op.listKey, list, newItem);
|
|
1452
1520
|
if (typeof newItem.id !== "string" ||
|
|
1453
1521
|
newItem.id.length === 0 ||
|
|
1454
1522
|
_itemIndexById(nextList, newItem.id) !== -1) {
|
|
@@ -1476,6 +1544,7 @@ async function _applyOpsAtomicallyUnsafe(session, ops, options) {
|
|
|
1476
1544
|
// A patch must never clobber the stable item id (it's the addressing key).
|
|
1477
1545
|
const itemPatch = { ...op.patch };
|
|
1478
1546
|
delete itemPatch.id;
|
|
1547
|
+
_requireKnownItemKeys(manifestByType, block.type, op.listKey, list, itemPatch);
|
|
1479
1548
|
const nextList = list.map((entry, idx) => {
|
|
1480
1549
|
if (idx !== targetIndex)
|
|
1481
1550
|
return entry;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"openai": "^4.87.1",
|
|
23
23
|
"sharp": "^0.35.4",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@avocadostudio-ai/migration-sdk": "^0.
|
|
26
|
-
"@avocadostudio-ai/shared": "^0.
|
|
25
|
+
"@avocadostudio-ai/migration-sdk": "^0.29.0",
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.29.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|