unoverse 0.1.199 → 0.1.201
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/lib/skills.mjs +32 -26
- package/operator/.env.example +1 -1
- package/operator/docker-compose.yml +1 -1
- package/operator/infra/aws/main.tf +1 -1
- package/operator/infra/digitalocean/main.tf +1 -1
- package/operator/lib/db-verify.sh +13 -10
- package/operator/lib/setup.sh +1 -1
- package/operator/operator.sh +1 -1
- package/package.json +1 -1
- package/vendor/base/items/collect.js +6 -2
- package/vendor/base/items/publish.js +1 -1
- package/vendor/base/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/lint/nodes/_schema/_defs.schema.json +2 -2
- package/vendor/base/lint/nodes/_schema/api.schema.json +2 -2
- package/vendor/base/lint/nodes/_schema/audio.schema.json +1 -1
- package/vendor/base/lint/nodes/_schema/config.schema.json +5 -5
- package/vendor/base/lint/nodes/_schema/credential.schema.json +3 -3
- package/vendor/base/lint/nodes/_schema/events.schema.json +6 -0
- package/vendor/base/lint/nodes/_schema/interface.schema.json +2 -2
- package/vendor/base/lint/nodes/_schema/narrate.schema.json +6 -0
- package/vendor/base/lint/nodes/_schema/node.schema.json +15 -7
- package/vendor/base/lint/nodes/_schema/package.schema.json +5 -5
- package/vendor/base/lint/nodes/_schema/run.schema.json +6 -0
- package/vendor/base/lint/nodes/_schema/service.schema.json +6 -0
- package/vendor/base/lint/nodes/_schema/test.schema.json +8 -8
- package/vendor/base/lint/nodes/_schema/toolExchange.schema.json +6 -0
- package/vendor/base/lint/nodes/index.mjs +1 -1
- package/vendor/base/lint/nodes/schema.mjs +7 -7
- package/vendor/base/manifests/source.js +1 -1
package/lib/skills.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Install the Claude Code authoring skills for this developer, FROM THE DOCS SITE.
|
|
2
2
|
//
|
|
3
|
-
// The skills are
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// The skills are files under packages/docs/skills/<skill>/, served by the docs site at the
|
|
4
|
+
// same path. They are NOT in the site's navigation (owner ruling 2026-09-05: a skill is
|
|
5
|
+
// for the agent to read, not a page for a person to browse), so the installer does not
|
|
6
|
+
// read llms.txt. It reads `skills/skills.json` for the list of skills, then each skill's
|
|
7
|
+
// SKILL.md, and follows every `references/<name>.md` the front page names. A reference the
|
|
8
|
+
// front page does not name never installs, and a test in packages/docs keeps the two in
|
|
9
|
+
// step.
|
|
8
10
|
//
|
|
9
11
|
// PER DEVELOPER, NOT PER PROJECT (2026-08-21). These install to ~/.claude/skills, so
|
|
10
12
|
// Claude Code picks them up in every folder the developer opens and nothing lands
|
|
@@ -20,18 +22,14 @@ import { tmpdir, homedir } from "node:os";
|
|
|
20
22
|
const DOCS = "https://docs.unoverse.ai";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
25
|
-
* whatever case the site serves it in, because Claude Code looks for exactly that name.
|
|
26
|
-
* Pure, so it can be checked without a network.
|
|
25
|
+
* The reference files a skill's front page names, as `references/<name>.md`, in order of
|
|
26
|
+
* first mention and without repeats. Pure, so it can be checked without a network.
|
|
27
27
|
*/
|
|
28
|
-
export function
|
|
28
|
+
export function referencesNamedBy(skillMd) {
|
|
29
29
|
const out = [];
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const rel = basename(path).toLowerCase() === "skill.md" ? join(dirname(path), "SKILL.md") : path;
|
|
34
|
-
out.push({ skill, rel, url });
|
|
30
|
+
for (const m of skillMd.matchAll(/references\/([a-z0-9-]+)\.md/g)) {
|
|
31
|
+
const rel = `references/${m[1]}.md`;
|
|
32
|
+
if (!out.includes(rel)) out.push(rel);
|
|
35
33
|
}
|
|
36
34
|
return out;
|
|
37
35
|
}
|
|
@@ -90,30 +88,38 @@ const fresh = (url) => `${url}?t=${Date.now()}`;
|
|
|
90
88
|
|
|
91
89
|
export async function installSkills() {
|
|
92
90
|
const target = join(homedir(), ".claude", "skills");
|
|
93
|
-
let
|
|
91
|
+
let skills;
|
|
94
92
|
try {
|
|
95
|
-
const res = await fetch(fresh(`${DOCS}/
|
|
93
|
+
const res = await fetch(fresh(`${DOCS}/skills/skills.json`));
|
|
96
94
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
97
|
-
|
|
95
|
+
skills = (await res.json()).skills ?? [];
|
|
98
96
|
} catch {
|
|
99
97
|
console.log(` Skills install skipped (${DOCS} unreachable). They install on the next update.`);
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
|
-
const skills = [...new Set(files.map((f) => f.skill))];
|
|
103
100
|
if (!skills.length) {
|
|
104
|
-
console.log(` Skills install skipped: ${DOCS}/
|
|
101
|
+
console.log(` Skills install skipped: ${DOCS}/skills/skills.json lists no skills.`);
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
// Fetched whole into a scratch folder first, so a download that dies halfway leaves the
|
|
108
105
|
// installed copy untouched rather than half-replaced.
|
|
109
106
|
const tmp = mkdtempSync(join(tmpdir(), "unoverse-skills-"));
|
|
110
107
|
try {
|
|
111
|
-
for (const
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
for (const skill of skills) {
|
|
109
|
+
const front = await fetch(fresh(`${DOCS}/skills/${skill}/SKILL.md`));
|
|
110
|
+
if (!front.ok) throw new Error(`${skill}/SKILL.md: HTTP ${front.status}`);
|
|
111
|
+
const frontText = await front.text();
|
|
112
|
+
const files = [["SKILL.md", frontText]];
|
|
113
|
+
for (const rel of referencesNamedBy(frontText)) {
|
|
114
|
+
const res = await fetch(fresh(`${DOCS}/skills/${skill}/${rel}`));
|
|
115
|
+
if (!res.ok) throw new Error(`${skill}/${rel}: HTTP ${res.status}`);
|
|
116
|
+
files.push([rel, await res.text()]);
|
|
117
|
+
}
|
|
118
|
+
for (const [rel, text] of files) {
|
|
119
|
+
const p = join(tmp, skill, rel);
|
|
120
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
121
|
+
writeFileSync(p, skillFileText(skill, rel, text));
|
|
122
|
+
}
|
|
117
123
|
}
|
|
118
124
|
mkdirSync(target, { recursive: true });
|
|
119
125
|
// REPLACED WHOLE, ours only. Each skill folder the site carries is removed and
|
package/operator/.env.example
CHANGED
|
@@ -42,7 +42,7 @@ DOCR_TOKEN=
|
|
|
42
42
|
# Page intelligence (promote-page extraction)
|
|
43
43
|
HYPERBROWSER_API_KEY=
|
|
44
44
|
|
|
45
|
-
# Marketplace catalogue to install from (docs/architecture/MARKETPLACE.md).
|
|
45
|
+
# Marketplace catalogue to install from (docs/architecture/authoring/MARKETPLACE.md).
|
|
46
46
|
# Empty = local items only. No default: a URL is never hardcoded.
|
|
47
47
|
UNOVERSE_MARKETPLACE_URL=
|
|
48
48
|
|
|
@@ -372,7 +372,7 @@ services:
|
|
|
372
372
|
restart: unless-stopped
|
|
373
373
|
|
|
374
374
|
# unoverse-runtime REMOVED 2026-07-28: WIP, excluded from the platform
|
|
375
|
-
# (docs/architecture/INFRASTRUCTURE.md). Returns when the user declares it ready.
|
|
375
|
+
# (docs/architecture/deployment/INFRASTRUCTURE.md). Returns when the user declares it ready.
|
|
376
376
|
|
|
377
377
|
# ===========================================================================
|
|
378
378
|
# OBSERVABILITY — log viewer (runs by default)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The Universe — AWS POC (docs/architecture/AWS_DEPLOYMENT.md)
|
|
1
|
+
# The Universe — AWS POC (docs/architecture/deployment/AWS_DEPLOYMENT.md)
|
|
2
2
|
#
|
|
3
3
|
# One VM + managed Postgres/Redis + Cognito + a scoped Bedrock IAM user.
|
|
4
4
|
# Deliberately flat and minimal: this is the POC tier. Terraform provisions,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The Universe — DigitalOcean ground (docs/architecture/INFRASTRUCTURE.md)
|
|
1
|
+
# The Universe — DigitalOcean ground (docs/architecture/deployment/INFRASTRUCTURE.md)
|
|
2
2
|
#
|
|
3
3
|
# Same five-input contract as infra/aws, DO implementation: Droplet + Managed
|
|
4
4
|
# Postgres (fronted by its built-in PgBouncer, per the Postgres law) + Managed
|
|
@@ -36,7 +36,7 @@ cmd_db_verify() {
|
|
|
36
36
|
"id", "name", "description", "nodes", "edges", "active",
|
|
37
37
|
"execution_mode", "test_inputs", "umap_settings", "viewport",
|
|
38
38
|
"mcp_schema", "memory_config", "content_taxonomy", "created_at", "updated_at",
|
|
39
|
-
"workflow_document", "layout_document"
|
|
39
|
+
"workflow_document", "layout_document", "org"
|
|
40
40
|
],
|
|
41
41
|
workflow_executions: [
|
|
42
42
|
"execution_id", "workflow_id", "status", "start_time", "end_time",
|
|
@@ -93,42 +93,42 @@ cmd_db_verify() {
|
|
|
93
93
|
"needs", "source_url", "source_id", "umap_x", "umap_y", "umap_z",
|
|
94
94
|
"umap_cluster_id", "color_hex", "needs_umap_update", "created_at",
|
|
95
95
|
"updated_at", "workflow_id", "key_need", "metadata", "embedding_original",
|
|
96
|
-
"cluster_distance"
|
|
96
|
+
"cluster_distance", "org"
|
|
97
97
|
],
|
|
98
98
|
dictionary_clusters: [
|
|
99
99
|
"cluster_id", "workflow_id", "parent_id", "depth", "name", "description",
|
|
100
100
|
"name_embedding", "medoid_id", "terms", "member_count", "umap_x", "umap_y",
|
|
101
101
|
"umap_z", "radius", "created_at", "updated_at",
|
|
102
102
|
"region_id", "carried_overlap", "derived_from", "name_locked", "locked_at",
|
|
103
|
-
"locked_by", "member_ids"
|
|
103
|
+
"locked_by", "member_ids", "org"
|
|
104
104
|
],
|
|
105
105
|
user_region_events: [
|
|
106
106
|
"id", "workflow_id", "user_id", "region_id", "region_name", "stage",
|
|
107
|
-
"source", "evidence", "occurred_at"
|
|
107
|
+
"source", "evidence", "occurred_at", "org"
|
|
108
108
|
],
|
|
109
109
|
dictionary_regions: [
|
|
110
110
|
"region_id", "workflow_id", "depth", "name", "description", "name_locked",
|
|
111
111
|
"locked_at", "locked_by", "stage", "stage_set_by", "needs_review",
|
|
112
112
|
"skills", "skills_set_by", "skills_set_at",
|
|
113
|
-
"derived_from", "first_seen", "last_seen", "closed_at"
|
|
113
|
+
"derived_from", "first_seen", "last_seen", "closed_at", "org"
|
|
114
114
|
],
|
|
115
115
|
dictionary_content_chunks: [
|
|
116
116
|
"chunk_id", "text", "source_url", "source_type", "metadata",
|
|
117
|
-
"workflow_id", "created_at", "updated_at", "embedding_original"
|
|
117
|
+
"workflow_id", "created_at", "updated_at", "embedding_original", "org"
|
|
118
118
|
],
|
|
119
119
|
dictionary_chunk_need_matches: [
|
|
120
120
|
"chunk_id", "need_id", "similarity_score", "rank", "match_type",
|
|
121
|
-
"created_at", "workflow_id"
|
|
121
|
+
"created_at", "workflow_id", "org"
|
|
122
122
|
],
|
|
123
123
|
dictionary_ingestion_configs: [
|
|
124
124
|
"id", "name", "description", "connector_type", "connector_config",
|
|
125
125
|
"main_category", "workflow_id", "created_at", "updated_at",
|
|
126
|
-
"last_run_at", "run_count"
|
|
126
|
+
"last_run_at", "run_count", "org"
|
|
127
127
|
],
|
|
128
128
|
dictionary_ingestion_jobs: [
|
|
129
129
|
"job_id", "workflow_id", "status", "connector_type", "category",
|
|
130
130
|
"config", "extraction_config", "started_at", "completed_at",
|
|
131
|
-
"progress", "error", "created_at"
|
|
131
|
+
"progress", "error", "created_at", "org"
|
|
132
132
|
],
|
|
133
133
|
goals: [
|
|
134
134
|
"goal_id", "user_id", "workflow_id", "status", "description",
|
|
@@ -141,13 +141,16 @@ cmd_db_verify() {
|
|
|
141
141
|
goal_scratch: [
|
|
142
142
|
"goal_id", "agent_id", "agent_name", "state", "updated_at"
|
|
143
143
|
],
|
|
144
|
+
orgs: [
|
|
145
|
+
"slug", "name", "settings", "website", "logo", "created_at", "updated_at"
|
|
146
|
+
],
|
|
144
147
|
knowledge_docs: [
|
|
145
148
|
"id", "workflow_id", "title", "doc_type", "sections", "version",
|
|
146
149
|
"links", "metadata", "created_at", "updated_at"
|
|
147
150
|
],
|
|
148
151
|
content_sources: [
|
|
149
152
|
"workflow_id", "connector", "source_key", "group_path", "label",
|
|
150
|
-
"status", "last_ingested", "metadata", "created_at", "updated_at"
|
|
153
|
+
"status", "last_ingested", "metadata", "created_at", "updated_at", "org"
|
|
151
154
|
],
|
|
152
155
|
security_attack_corpus: [
|
|
153
156
|
"id", "category", "label", "attack_prompt", "expected_result",
|
package/operator/lib/setup.sh
CHANGED
|
@@ -343,7 +343,7 @@ OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
|
343
343
|
# back it up with the database. Kept on re-runs.
|
|
344
344
|
CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
|
|
345
345
|
HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
|
|
346
|
-
# Marketplace catalogue to install from (docs/architecture/MARKETPLACE.md).
|
|
346
|
+
# Marketplace catalogue to install from (docs/architecture/authoring/MARKETPLACE.md).
|
|
347
347
|
# Empty = local items only. No default: a URL is never hardcoded.
|
|
348
348
|
UNOVERSE_MARKETPLACE_URL=
|
|
349
349
|
DOMAIN=
|
package/operator/operator.sh
CHANGED
|
@@ -223,7 +223,7 @@ case "${1:-}" in
|
|
|
223
223
|
echo "" >&2
|
|
224
224
|
echo " scripts/operator.sh publish" >&2
|
|
225
225
|
echo "" >&2
|
|
226
|
-
echo " See docs/architecture/DEVELOPER_GUIDE.md § Releasing." >&2
|
|
226
|
+
echo " See docs/architecture/platform/DEVELOPER_GUIDE.md § Releasing." >&2
|
|
227
227
|
echo "" >&2
|
|
228
228
|
exit 1
|
|
229
229
|
else
|
package/package.json
CHANGED
|
@@ -97,7 +97,8 @@ export function collectProject(designRoot, project) {
|
|
|
97
97
|
// A prompt block is NEVER qualified this way — see the blocks/ walk below for why.
|
|
98
98
|
// Apps keep their bare ids: those are org-qualified by convention
|
|
99
99
|
// (`<org>-chat`). docs/unoverse/UNOVERSE_COMPONENT_ORGS.md.
|
|
100
|
-
|
|
100
|
+
// `identity` (2026-09-05): the org's four documents, `<org>/brand` like a component.
|
|
101
|
+
const QUALIFIED_KINDS = new Set(["component", "skill", "template", "identity"]);
|
|
101
102
|
const add = (kind, name, definition) => items.push({
|
|
102
103
|
kind,
|
|
103
104
|
name: QUALIFIED_KINDS.has(kind) ? `${project}/${name}` : name,
|
|
@@ -121,7 +122,10 @@ export function collectProject(designRoot, project) {
|
|
|
121
122
|
// 029): apps publish as kind `app` and the Template Model kind owns the bare
|
|
122
123
|
// word. Forgetting a design dir here ships deploys that silently drop every
|
|
123
124
|
// artifact of that kind.
|
|
124
|
-
|
|
125
|
+
// IDENTITY (owner ruling 2026-09-05): design/<org>/identity/<doc>/ publishes as kind
|
|
126
|
+
// `identity`, the same walk as a component folder, so it reaches the registry, the
|
|
127
|
+
// spatial catalogue and the map through the one publish path everything else takes.
|
|
128
|
+
for (const [dir, kind] of [["components", "component"], ["apps", "app"], ["templates", "template"], ["atoms", "atom"], ["identity", "identity"]]) {
|
|
125
129
|
const home = join(root, dir);
|
|
126
130
|
if (!existsSync(home))
|
|
127
131
|
continue;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* new, changed, unchanged, or refused because the name belongs to someone else. Publishing
|
|
11
11
|
* blind is how a project half-lands and nobody can tell by looking.
|
|
12
12
|
*
|
|
13
|
-
* See docs/architecture/DECLARATIVE_NODES.md §9.
|
|
13
|
+
* See docs/architecture/authoring/DECLARATIVE_NODES.md §9.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
@@ -57,7 +57,7 @@ export const PROP_KEYS = [
|
|
|
57
57
|
*/
|
|
58
58
|
export const INTERFACE_FIELDS = new Set([
|
|
59
59
|
"universal_id", "title", "description", "object_type", "key_need", "source_url", "source_id",
|
|
60
|
-
"tagline", "shortDescription", "introParagraph", "callToAction", "
|
|
60
|
+
"tagline", "shortDescription", "introParagraph", "callToAction", "primaryImage", "images", "action",
|
|
61
61
|
"bodyCopy", "features", "mainCategory", "section", "needs",
|
|
62
62
|
]);
|
|
63
63
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/_defs.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/_defs.schema.json",
|
|
4
4
|
"title": "Shared node definitions",
|
|
5
|
-
"description": "Fragments $ref'd by the sibling node schemas. Not authored directly. See docs/architecture/DECLARATIVE_NODES.md.",
|
|
5
|
+
"description": "Fragments $ref'd by the sibling node schemas. Not authored directly. See docs/architecture/authoring/DECLARATIVE_NODES.md.",
|
|
6
6
|
"definitions": {
|
|
7
7
|
"nodeType": {
|
|
8
8
|
"type": "string",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/api.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/api.schema.json",
|
|
4
4
|
"title": "Unoverse node API call",
|
|
5
|
-
"description": "api.yaml — the upstream call this node makes, and how its response maps onto the node's outputs. This is the file that replaces an executor. NAMED api, not service: `service` already means serviceConnectors / isService / MCP providers / /service-call in this platform. See docs/architecture/DECLARATIVE_NODES.md §8.\n\nEVERY enum below is the EXECUTOR'S CAPABILITY LIST. A manifest may only name a capability that is already implemented in code. Adding a value here without implementing it produces a node that lints clean and fails at run time, and allowing a manifest to name arbitrary code would end the safety property this whole format exists for (§2).\n\nA node is reached two ways, and they never cross. The GRAPH RUNS it: `run` is the ordered list of calls it makes, and `events` says what lands on its output connectors. Or a SERVICE CALLS it ad-hoc, exposed as MCP: `service` holds those methods, each with its own list of calls, each handing one value straight back to the caller rather than touching a connector. A pure service node has only `service`.\n\nCalls are ALWAYS a `run` list, even when there is one of them. There is no `request` key.",
|
|
5
|
+
"description": "api.yaml — the upstream call this node makes, and how its response maps onto the node's outputs. This is the file that replaces an executor. NAMED api, not service: `service` already means serviceConnectors / isService / MCP providers / /service-call in this platform. See docs/architecture/authoring/DECLARATIVE_NODES.md §8.\n\nEVERY enum below is the EXECUTOR'S CAPABILITY LIST. A manifest may only name a capability that is already implemented in code. Adding a value here without implementing it produces a node that lints clean and fails at run time, and allowing a manifest to name arbitrary code would end the safety property this whole format exists for (§2).\n\nA node is reached two ways, and they never cross. The GRAPH RUNS it: `run` is the ordered list of calls it makes, and `events` says what lands on its output connectors. Or a SERVICE CALLS it ad-hoc, exposed as MCP: `service` holds those methods, each with its own list of calls, each handing one value straight back to the caller rather than touching a connector. A pure service node has only `service`.\n\nCalls are ALWAYS a `run` list, even when there is one of them. There is no `request` key.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [],
|
|
8
8
|
"definitions": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/audio.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/audio.schema.json",
|
|
4
4
|
"title": "Unoverse node audio lane",
|
|
5
5
|
"description": "api/audio.yaml — how a duplex voice node is wired to the platform's audio lane. Only meaningful beside a `transport: ws` call.\n\nWHY THIS FILE EXISTS, and it is not a design preference. There are TWO sockets in a voice call. The VENDOR socket lives in api/run.yaml and is the node's conversation with OpenAI, xAI or Nova. The AUDIO LANE is the platform's own socket to the browser, and it exists for exactly ONE reason: MCP cannot carry binary audio.\n\nSo the rule that keeps this file small: EVERYTHING THAT IS NOT AUDIO BELONGS IN `events`. Transcripts, tool results, speech state, usage — all of it reaches the client over MCP streaming already, by landing on an output connector like any other node's output. Nothing needs a side channel to get there. If something non-audio is being put in this file, the reason is almost always that it was easier to reach the lane than to declare an output, and that is the wrong trade: an output is visible on the canvas, wirable, and testable, and a side channel is none of those.\n\nWHAT THE EXECUTOR CONTRIBUTES. Everything here names computation rather than describing data, which is why it is a handful of keys rather than a body template. Resampling is an interpolation over samples. Coalescing is a timer and a byte budget. Barge-in is discarding a buffer rather than flushing it. None of those can be written as an expression, and all of them are identical for every vendor, which is exactly the test for belonging to the executor (DECLARATIVE_NODES.md §2).",
|
|
6
6
|
"type": "object",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/config.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/config.schema.json",
|
|
4
4
|
"title": "Unoverse node config",
|
|
5
|
-
"description": "config.yaml
|
|
5
|
+
"description": "config.yaml — the node's settings form. `configSchema` is itself a JSON Schema: this file describes the SHAPE OF THAT SCHEMA, not the shape of a config. Canvas renders the form from it and the executor resolves {{ config.* }} against the saved values.\n\nThis is the file with the most churn: every new option lands here and nowhere else. Full reference for the options: docs-starter/nodes/config-schema.md.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"configSchema"
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"enum": [
|
|
117
117
|
"modelTier"
|
|
118
118
|
],
|
|
119
|
-
"description": "An executor RESOLVER applied to this field's saved value just before the request is built. IMPLEMENTED resolvers only.\n\n`modelTier` is the migration of shared/models.ts resolveModel(): a saved workflow stores a concrete model id, and if that id is later retired the resolver falls back to the current best model in the same tier, warning the operator. Without it, replacing a model generation breaks every saved workflow that named an old id.\n\nThis is the
|
|
119
|
+
"description": "An executor RESOLVER applied to this field's saved value just before the request is built. IMPLEMENTED resolvers only.\n\n`modelTier` is the migration of shared/models.ts resolveModel(): a saved workflow stores a concrete model id, and if that id is later retired the resolver falls back to the current best model in the same tier, warning the operator. Without it, replacing a model generation breaks every saved workflow that named an old id.\n\nThis is the §2 line in miniature: the model LIST is description and lives in shared/models.yaml, while the fallback is computation and lives in the executor.",
|
|
120
120
|
"$comment": "Adding a value here without implementing it produces a node that lints clean and fails at run time."
|
|
121
121
|
},
|
|
122
122
|
"ui:field": {
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"checkboxes",
|
|
138
138
|
"domainSelector"
|
|
139
139
|
],
|
|
140
|
-
"description": "IMPLEMENTED widgets only, verified against the renderer in apps/canvas/src/components/workflow/ConfigurationForm/FieldRenderer.jsx:\n\n toggle a boolean switch\n select an enum picker\n textarea a multi-line box (FieldRenderer.jsx:248). Pair with ui:options.rows\n slider a number, dragged rather than typed (:162)\n checkboxes an array of enum values, all visible at once (:208)\n domainSelector an array of hostnames (:195)\n\nNOT `color`. The retired Note node declared `ui:widget: color` and the renderer has never implemented it, so it fell through to a plain text input
|
|
140
|
+
"description": "IMPLEMENTED widgets only, verified against the renderer in apps/canvas/src/components/workflow/ConfigurationForm/FieldRenderer.jsx:\n\n toggle a boolean switch\n select an enum picker\n textarea a multi-line box (FieldRenderer.jsx:248). Pair with ui:options.rows\n slider a number, dragged rather than typed (:162)\n checkboxes an array of enum values, all visible at once (:208)\n domainSelector an array of hostnames (:195)\n\nNOT `color`. The retired Note node declared `ui:widget: color` and the renderer has never implemented it, so it fell through to a plain text input — a setting that looked bespoke and was not. Adding a value here without a branch in FieldRenderer reproduces exactly that: it lints clean and silently renders as something else."
|
|
141
141
|
},
|
|
142
142
|
"ui:dependencies": {
|
|
143
143
|
"type": "object",
|
|
@@ -171,4 +171,4 @@
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
}
|
|
174
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/credential.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/credential.schema.json",
|
|
4
4
|
"title": "Unoverse credential type",
|
|
5
|
-
"description": "credentials/<name>.yaml
|
|
5
|
+
"description": "credentials/<name>.yaml — the SHAPE of a credential, never its value. Declared once per package and referenced by name from each node.yaml.\n\nSecrets stay where they already live: encrypted in Postgres via credentialManager on a deployed universe, and in the developer's own .env locally (LOCAL_STUDIO.md). Only the schema is data in a manifest, which is why a manifest is safe to commit and to deploy.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"name",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"name": {
|
|
45
45
|
"type": "string",
|
|
46
46
|
"pattern": "^[a-z][A-Za-z0-9_]*$",
|
|
47
|
-
"description": "The FIELD name, as a manifest reads it: credentials.<credentialName>.<fieldName>.\n\ncamelCase by convention. snake_case is ALLOWED, for exactly the reason a PORT name may be snake_case (_defs.schema.json): a migrated node must keep the names its code version used. A credential is read field by field out of stored, encrypted values
|
|
47
|
+
"description": "The FIELD name, as a manifest reads it: credentials.<credentialName>.<fieldName>.\n\ncamelCase by convention. snake_case is ALLOWED, for exactly the reason a PORT name may be snake_case (_defs.schema.json): a migrated node must keep the names its code version used. A credential is read field by field out of stored, encrypted values — Cloudinary's are cloud_name, api_key and api_secret — so tidying them to camelCase would leave every existing workflow authenticating with undefined, and it would fail as a vendor 401 rather than as anything naming the rename. Fidelity beats house style here.\n\nPrefer camelCase for a NEW credential, where nothing is stored yet."
|
|
48
48
|
},
|
|
49
49
|
"displayName": {
|
|
50
50
|
"type": "string"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/interface.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/interface.schema.json",
|
|
4
4
|
"title": "Unoverse node interface",
|
|
5
|
-
"description": "interface.yaml — the node's WIRING SURFACE, and nothing else. Everything another node, an agent, or a developer needs in order to connect to this one: what goes in, what comes out, what services it offers or consumes, and what credentials it needs.\n\nIt has its own file because this is the question asked most often about a node, and it should never require reading past a node's branding to answer. See docs/architecture/DECLARATIVE_NODES.md §5.",
|
|
5
|
+
"description": "interface.yaml — the node's WIRING SURFACE, and nothing else. Everything another node, an agent, or a developer needs in order to connect to this one: what goes in, what comes out, what services it offers or consumes, and what credentials it needs.\n\nIt has its own file because this is the question asked most often about a node, and it should never require reading past a node's branding to answer. See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"$schema": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/node.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/node.schema.json",
|
|
4
4
|
"title": "Unoverse node",
|
|
5
|
-
"description": "node.yaml — what this node IS: its identity, its kind, and how it is discovered. The only required file in a node folder.\n\nThe wiring surface lives next door in interface.yaml, so \"what can I connect to this?\" is answered without reading past the node's branding. config/api/test/interface may each be inlined here instead of living in their own file, but never both (that is a lint error, not a merge). See docs/architecture/DECLARATIVE_NODES.md §5.",
|
|
5
|
+
"description": "node.yaml — what this node IS: its identity, its kind, and how it is discovered. The only required file in a node folder.\n\nThe wiring surface lives next door in interface.yaml, so \"what can I connect to this?\" is answered without reading past the node's branding. config/api/test/interface may each be inlined here instead of living in their own file, but never both (that is a lint error, not a merge). See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"type",
|
|
@@ -89,23 +89,31 @@
|
|
|
89
89
|
"cacheable": {
|
|
90
90
|
"description": "Output may be MEMOIZED: the engine may serve a prior run's output when the fingerprint (type + version + RESOLVED config + credential ref + scope) matches.\n\nSet ONLY for idempotent side-effect-free READS (search, scrape, fetch-by-id, pure transform). NEVER for effectful nodes (send, write, post, charge) where reuse silently skips the side effect, and NEVER for non-deterministic ones (LLM completions, time or random dependent) where re-running is the correct behaviour. Memoization is off engine-wide by default; this flag only makes the node eligible.\n\nOBJECT FORM, for nodes whose config carries a VOLATILE field (a presigned/expiring URL) over content whose identity arrives on an input (an etag). `ignore` drops the named top-level resolved-config fields from the fingerprint; `key` names input leaf fields (dot-suffix match, e.g. \"etag\" or \"file.etag\") the engine collects from the resolved inputs as the content's identity. If a key field collects NOTHING on a run, that run is not cached — no identity, no reuse. Declaring the object form IS the opt-in.",
|
|
91
91
|
"oneOf": [
|
|
92
|
-
{
|
|
92
|
+
{
|
|
93
|
+
"type": "boolean"
|
|
94
|
+
},
|
|
93
95
|
{
|
|
94
96
|
"type": "object",
|
|
95
97
|
"properties": {
|
|
96
98
|
"ignore": {
|
|
97
99
|
"type": "array",
|
|
98
|
-
"items": {
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "string"
|
|
102
|
+
},
|
|
99
103
|
"description": "Top-level resolved-config fields excluded from the fingerprint because they are volatile, not identity (a presigned URL that changes every run). Every OTHER config field still busts the cache."
|
|
100
104
|
},
|
|
101
105
|
"key": {
|
|
102
106
|
"type": "array",
|
|
103
|
-
"items": {
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
},
|
|
104
110
|
"minItems": 1,
|
|
105
111
|
"description": "Input leaf fields that identify the content, matched by dot-suffix against every leaf path in the resolved inputs (wiring-independent: \"etag\" matches inputs.file.<anyNode>.file.etag). All matches, sorted by path, enter the fingerprint. A key that matches nothing disables caching for that run."
|
|
106
112
|
}
|
|
107
113
|
},
|
|
108
|
-
"required": [
|
|
114
|
+
"required": [
|
|
115
|
+
"key"
|
|
116
|
+
],
|
|
109
117
|
"additionalProperties": false
|
|
110
118
|
}
|
|
111
119
|
]
|
|
@@ -135,7 +143,7 @@
|
|
|
135
143
|
},
|
|
136
144
|
"auth": {
|
|
137
145
|
"type": "object",
|
|
138
|
-
"description": "WHO MAY RUN THIS NODE — inbound, about the caller (docs/architecture/DECLARATIVE_NODES.md §9.13). Not to be confused with a call's `credential` in api/run.yaml, which is outbound: how the node proves itself to a vendor. Both were spelled `auth` until 2026-07-28.\n\nCOMPULSORY, which is the point. It was optional (`requires: { role }`) until 2026-07-28, and every node in the tree said nothing — so a node that had been considered and left open looked exactly like a node nobody had thought about. Silence is not an answer to this question.",
|
|
146
|
+
"description": "WHO MAY RUN THIS NODE — inbound, about the caller (docs/architecture/authoring/DECLARATIVE_NODES.md §9.13). Not to be confused with a call's `credential` in api/run.yaml, which is outbound: how the node proves itself to a vendor. Both were spelled `auth` until 2026-07-28.\n\nCOMPULSORY, which is the point. It was optional (`requires: { role }`) until 2026-07-28, and every node in the tree said nothing — so a node that had been considered and left open looked exactly like a node nobody had thought about. Silence is not an answer to this question.",
|
|
139
147
|
"additionalProperties": false,
|
|
140
148
|
"properties": {
|
|
141
149
|
"required": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/package.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/package.schema.json",
|
|
4
4
|
"title": "Unoverse node package",
|
|
5
|
-
"description": "package.yaml
|
|
5
|
+
"description": "package.yaml — the envelope for a folder of nodes. Replaces the `gravity` block in package.json for packages that no longer ship code. See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"name",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"type": "string",
|
|
62
62
|
"pattern": "^(\\*$|(\\*\\*\\.|\\*\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+$)"
|
|
63
63
|
},
|
|
64
|
-
"description": "The ONLY hosts this package's nodes may call. The executor refuses any other, and lint refuses a request URL outside it.\n\nThis is the control that makes a manifest safe to accept from someone else. A manifest cannot execute, but it CAN say \"POST this credential to evil.example\", and that is exfiltration without a line of code. Bounding allowedHosts means a package's secrets can only ever reach hosts it declared in writing, and the declaration is one short reviewable list at install time.\n\nSECURITY.md draws the line as: a NODE is trusted code bounded by provenance; a TEMPLATE EXPRESSION is untrusted data bounded by having no credentials in scope. A manifest node is neither
|
|
64
|
+
"description": "The ONLY hosts this package's nodes may call. The executor refuses any other, and lint refuses a request URL outside it.\n\nThis is the control that makes a manifest safe to accept from someone else. A manifest cannot execute, but it CAN say \"POST this credential to evil.example\", and that is exfiltration without a line of code. Bounding allowedHosts means a package's secrets can only ever reach hosts it declared in writing, and the declaration is one short reviewable list at install time.\n\nSECURITY.md draws the line as: a NODE is trusted code bounded by provenance; a TEMPLATE EXPRESSION is untrusted data bounded by having no credentials in scope. A manifest node is neither — it arrives as data but holds a credential and a URL. AllowedHosts is its boundary.\n\n`*.` matches ONE subdomain level, e.g. \"*.slack.com\" reaches api.slack.com but not a.b.slack.com.\n\n`**.` matches ANY depth, and is written with two stars because it is a deliberately weaker claim the author should have to make on purpose. AWS forced it: `dynamodb.us-east-1.amazonaws.com` is two labels deep and an S3 bucket is three, since the region AND the bucket are part of the host. Listing every region defeats itself the day AWS adds one, and a bucket name comes from config so it cannot be listed at all. Still bounded: \"**.amazonaws.com\" reaches any AWS host and nothing else.\n\n`*` alone means ANY HOST, and it applies ONLY to calls that send no credential. Some nodes legitimately fetch a url a person supplies (an image for a model to look at, a document to read) and that url cannot be declared in advance. It is safe exactly there: this boundary exists to stop exfiltration, and a call with no `auth` block has no credential to leak. Non-https is still refused, so cloud metadata and plaintext internal services stay out of reach, and the moment a call carries a credential the wildcard stops matching and the declared list applies.",
|
|
65
65
|
"examples": [
|
|
66
66
|
[
|
|
67
67
|
"api.openai.com"
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
"poll": {
|
|
135
135
|
"type": "boolean",
|
|
136
|
-
"description": "True when a node waits on an asynchronous JOB: start it, then ask until it is done. On an executor without it the node settles on the START reply, which is a receipt carrying a job id, and emits that as its answer
|
|
136
|
+
"description": "True when a node waits on an asynchronous JOB: start it, then ask until it is done. On an executor without it the node settles on the START reply, which is a receipt carrying a job id, and emits that as its answer — every downstream field reads empty and nothing errors."
|
|
137
137
|
},
|
|
138
138
|
"encoding": {
|
|
139
139
|
"type": "array",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"ndjson"
|
|
146
146
|
]
|
|
147
147
|
},
|
|
148
|
-
"description": "Body encodings this package's nodes use. Declared like auth and transport, and for the same reason: on an executor without one, the body is sent as plain JSON instead. That is not an error anywhere
|
|
148
|
+
"description": "Body encodings this package's nodes use. Declared like auth and transport, and for the same reason: on an executor without one, the body is sent as plain JSON instead. That is not an error anywhere — the vendor simply rejects a shape it did not expect, or worse accepts it and ignores the file, so an upload silently transcribes nothing."
|
|
149
149
|
},
|
|
150
150
|
"renderComponents": {
|
|
151
151
|
"type": "boolean",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://unoverse/nodes/test.schema.json",
|
|
3
|
+
"$id": "https://docs.unoverse.ai/schemas/nodes/test.schema.json",
|
|
4
4
|
"title": "Unoverse node test data",
|
|
5
|
-
"description": "test.yaml
|
|
5
|
+
"description": "test.yaml — the fixture behind Studio's \"Load sample, Run\" button and behind `unoverse node test`. Every node must have one: a node nobody can run is a node nobody can trust, and lint enforces it.\n\nTemplate fields are supplied ALREADY RESOLVED, because the bench has no upstream node to resolve them from. See docs/architecture/authoring/DECLARATIVE_NODES.md §6.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"testData"
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"additionalProperties": false
|
|
43
43
|
},
|
|
44
44
|
"state": {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "PLATFORM STORAGE this fixture starts with, keyed by LOGICAL key. The bench has no Redis, so it runs the real state and loop code against an in-memory client seeded from here; a value is either an object (stored whole) or an array (stored as a list, for drain).\n\nIt exists because some nodes only make sense with a warm store. `LoopEnd` closes a pass of a loop that `LoopStart` opened, so with nothing seeded its very first act is the \"loop state not found\" error it is supposed to raise for a mistyped id — the bench would report a real bug as the node's normal behaviour. Same for any node whose first call is a cache read.\n\nThe run's ids are FIXED on the bench (see test-node.mjs) so a key naming them is predictable.",
|
|
47
|
+
"additionalProperties": true
|
|
48
|
+
},
|
|
49
|
+
"expect": {
|
|
50
50
|
"type": "object",
|
|
51
51
|
"description": "Assertions over the result. On the workflow channel the scope is `output` (the emitted connectors); on the service channel it is `output` too, bound to the value the method RETURNED. Absent means the run only has to succeed.",
|
|
52
52
|
"additionalProperties": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"required": [
|
|
59
59
|
"method"
|
|
60
60
|
],
|
|
61
|
-
"description": "For a node with `provides`: which SERVICE method to exercise and with what arguments. A pure service node has no inputs to feed and no outputs to watch, so `inputs` cannot stand in
|
|
61
|
+
"description": "For a node with `provides`: which SERVICE method to exercise and with what arguments. A pure service node has no inputs to feed and no outputs to watch, so `inputs` cannot stand in — the only way to run it is to call it the way a consumer would.",
|
|
62
62
|
"properties": {
|
|
63
63
|
"method": {
|
|
64
64
|
"type": "string",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* without it is the gap LOCAL_STUDIO.md:246 names: "a malformed definition is one restart
|
|
14
14
|
* from a broken server", and a published row does not even need the restart.
|
|
15
15
|
*
|
|
16
|
-
* FOUR TIERS (docs/architecture/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
|
|
16
|
+
* FOUR TIERS (docs/architecture/authoring/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
|
|
17
17
|
* 1 editor $schema pointers + .vscode yaml.schemas, no tool
|
|
18
18
|
* 2 structural every part validated against the node schemas (packages/docs/schemas/nodes)
|
|
19
19
|
* 3 semantic the cross-file rules a schema cannot express
|
|
@@ -25,13 +25,13 @@ try {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const SCHEMA_ID = {
|
|
28
|
-
node: "https://unoverse/nodes/node.schema.json",
|
|
29
|
-
interface: "https://unoverse/nodes/interface.schema.json",
|
|
30
|
-
config: "https://unoverse/nodes/config.schema.json",
|
|
31
|
-
api: "https://unoverse/nodes/api.schema.json",
|
|
32
|
-
test: "https://unoverse/nodes/test.schema.json",
|
|
33
|
-
package: "https://unoverse/nodes/package.schema.json",
|
|
34
|
-
credential: "https://unoverse/nodes/credential.schema.json",
|
|
28
|
+
node: "https://docs.unoverse.ai/schemas/nodes/node.schema.json",
|
|
29
|
+
interface: "https://docs.unoverse.ai/schemas/nodes/interface.schema.json",
|
|
30
|
+
config: "https://docs.unoverse.ai/schemas/nodes/config.schema.json",
|
|
31
|
+
api: "https://docs.unoverse.ai/schemas/nodes/api.schema.json",
|
|
32
|
+
test: "https://docs.unoverse.ai/schemas/nodes/test.schema.json",
|
|
33
|
+
package: "https://docs.unoverse.ai/schemas/nodes/package.schema.json",
|
|
34
|
+
credential: "https://docs.unoverse.ai/schemas/nodes/credential.schema.json",
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/** The four sections that may live in their own file OR inline in node.yaml. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Everything past this file works on the composed definition and never learns where
|
|
10
10
|
* it came from.
|
|
11
11
|
*
|
|
12
|
-
* See docs/architecture/DECLARATIVE_NODES.md.
|
|
12
|
+
* See docs/architecture/authoring/DECLARATIVE_NODES.md.
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as path from "path";
|