-
Notifications
You must be signed in to change notification settings - Fork 2
feat(workflow): materialize certified tool capabilities as nodes #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
87e100c
1e9f5b9
592efb2
1749ec5
1d8b0e8
3d43050
17e8736
289fd95
a1ec917
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -231,6 +231,8 @@ def resolve_node_origin(node: WorkflowProjectNode) -> WorkflowNodeOrigin: | |
|
|
||
| if catalog_id in WORKFLOW_CATALOG_IDS: | ||
| return WorkflowNodeOrigin(kind="node_library", catalog_id=catalog_id) | ||
| if _is_registered_tool_capability_node(node, catalog_id): | ||
| return WorkflowNodeOrigin(kind="node_library", catalog_id=catalog_id) | ||
| if primitive_id in WORKFLOW_PRIMITIVE_IDS: | ||
| return WorkflowNodeOrigin(kind="primitive_library", primitive_id=primitive_id) | ||
| if n8n is not None: | ||
|
|
@@ -250,6 +252,23 @@ def resolve_node_origin(node: WorkflowProjectNode) -> WorkflowNodeOrigin: | |
| return WorkflowNodeOrigin(kind="legacy", missing_capability=missing_capability, notes=notes) | ||
|
|
||
|
|
||
| def _is_registered_tool_capability_node( | ||
| node: WorkflowProjectNode, | ||
| catalog_id: str | None, | ||
| ) -> bool: | ||
| tool = node.params.get("toolCapability") | ||
| if not isinstance(tool, dict): | ||
| return False | ||
| tool_id = _read_string(tool.get("id")) | ||
| if tool_id is None or catalog_id != tool_id: | ||
| return False | ||
|
|
||
| # Local import avoids making the registry depend on this origin guard at import time. | ||
| from backend.workflow.tool_capabilities import resolve_workflow_tool_capability | ||
|
|
||
| return resolve_workflow_tool_capability(tool_id) is not None | ||
|
Comment on lines
+255
to
+269
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Inspect how WorkflowNodeOrigin.kind == "node_library" is consumed downstream
rg -n -B2 -A6 'node_library' backend/workflow/compiler.py backend/workflow/runtime_registry.pyRepository: 2233admin/opencli-admin Length of output: 4707 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files 'backend/workflow/*.py' | sed -n '1,120p'
echo "== node_registry origin functions =="
fgrep -n "_is_registered_tool_capability_node\|resolve_node_origin\|_tool_catalog_capabilities\|workflow_tool_capability\|nodeCatalog\|canvas.node" backend/workflow/node_registry.py backend/workflow/capability_projection.py backend/workflow/tool_capabilities.py backend/workflow/runtime_registry.py backend/workflow/compiler.py 2>/dev/null | sed -n '1,240p'
echo "== imports == capability_projection.py =="
sed -n '1,220p' backend/workflow/capability_projection.py
echo "== tool_capabilities.py outline/imports =="
sed -n '1,220p' backend/workflow/tool_capabilities.pyRepository: 2233admin/opencli-admin Length of output: 2473 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== relevant lines node_registry.py =="
sed -n '230,285p' backend/workflow/node_registry.py
echo "== resolve origin usage =="
rg -n -B3 -A8 "resolve_node_origin|WorkflowNodeOrigin|external.tool.capability|tool_capability|node_library" backend/workflow/compiler.py backend/workflow/runtime_registry.py backend/workflow/node_registry.py
echo "== capability_projection.py first 220 =="
sed -n '1,220p' backend/workflow/capability_projection.py
echo "== tool_capabilities.py first 220 =="
sed -n '1,220p' backend/workflow/tool_capabilities.pyRepository: 2233admin/opencli-admin Length of output: 49698 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== tool catalog projection lines =="
rg -n -B8 -A18 "_tool_catalog_capabilities|canvas\.node|nodeCatalog|authority|external.tool.capability|tool.intelligence.native" backend/workflow/capability_projection.py backend/workflow/tool_capabilities.py
echo "== resolved external tool capability flow =="
sed -n '133,150p' backend/workflow/runtime_registry.py
sed -n '784,814p' backend/workflow/runtime_registry.py
sed -n '1136,1143p' backend/workflow/runtime_registry.py
echo "== behavioral probe: node origin vs catalog eligibility data =="
python3 - <<'PY'
from pathlib import Path
import ast
node_registry = Path("backend/workflow/node_registry.py").read_text()
catalog = Path("backend/workflow/capability_projection.py").read_text()
node_origin_tree = ast.parse(node_registry)
catalog_tree = ast.parse(catalog)
for name in ("_is_registered_tool_capability_node", "_tool_catalog_capabilities"):
for fn in ast.walk(node_origin_tree if name == "_is_registered_tool_capability_node" else catalog_tree):
if isinstance(fn, ast.FunctionDef) and fn.name == name:
print(f"== {name} ==")
print(Path("backend/workflow/node_registry.py" if name == "_is_registered_tool_capability_node" else "backend/workflow/capability_projection.py").read_text().splitlines()[fn.lineno-1:fn.end_lineno])
print("canvas.node occurrence:", fn.lineno <= next((line for line in ast.get_source_segment(catalog if name == "_tool_catalog_capabilities" else node_registry, fn) or "").splitlines() + ["no match"], ["no match"]))
PYRepository: 2233admin/opencli-admin Length of output: 25270 Align tool capability origin checks with catalog opt-in eligibility.
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def forbidden_node_definition_keys(node: WorkflowProjectNode) -> list[str]: | ||
| """Return raw implementation keys that are never valid workflow authoring data.""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import assert from "node:assert/strict" | ||
| import { existsSync, readFileSync } from "node:fs" | ||
| import { registerHooks, stripTypeScriptTypes } from "node:module" | ||
| import { test } from "node:test" | ||
| import { fileURLToPath, pathToFileURL } from "node:url" | ||
| import path from "node:path" | ||
|
|
||
| const frontendRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") | ||
|
|
||
| registerHooks({ | ||
| resolve(specifier, context, nextResolve) { | ||
| const candidates = [] | ||
| if (specifier.startsWith("@/")) { | ||
| candidates.push(path.join(frontendRoot, specifier.slice(2))) | ||
| } else if (specifier.startsWith(".") && context.parentURL?.startsWith("file:")) { | ||
| candidates.push(path.resolve(path.dirname(fileURLToPath(context.parentURL)), specifier)) | ||
| } | ||
| for (const candidate of candidates) { | ||
| for (const resolvedPath of [candidate, `${candidate}.ts`, `${candidate}.tsx`]) { | ||
| if (existsSync(resolvedPath)) { | ||
| return { url: pathToFileURL(resolvedPath).href, shortCircuit: true } | ||
| } | ||
| } | ||
| } | ||
| return nextResolve(specifier, context) | ||
| }, | ||
| load(url, context, nextLoad) { | ||
| if (url.endsWith(".ts") || url.endsWith(".tsx")) { | ||
| const source = stripTypeScriptTypes(readFileSync(fileURLToPath(url), "utf8"), { | ||
| mode: "strip", | ||
| sourceUrl: url, | ||
| }) | ||
| return { format: "module", source, shortCircuit: true } | ||
| } | ||
| return nextLoad(url, context) | ||
| }, | ||
| }) | ||
|
|
||
| test("backend tool capability becomes an executable catalog node", async () => { | ||
| const { createWorkflowNodeFromCatalog, getWorkflowNodeCatalog } = await import( | ||
| pathToFileURL(path.join(frontendRoot, "lib/workflow/node-catalog.ts")).href | ||
| ) | ||
| const toolCapability = { | ||
| id: "tool.osint.metasearch", | ||
| versionPin: { | ||
| package: "opencli-admin", | ||
| packageVersion: "0.1.0", | ||
| capabilityVersion: "1.0.0", | ||
| provenance: "built-in", | ||
| }, | ||
| executor: { mode: "fixture", params: { limit: 20 } }, | ||
| } | ||
| const runtimeCapability = { | ||
| id: "tool.osint.metasearch", | ||
| label: "OSINT Metasearch", | ||
| surface: "catalog", | ||
| status: "runnable", | ||
| backendAvailable: true, | ||
| kind: "action", | ||
| capability: "store", | ||
| provider: "opencli-admin", | ||
| runtimeBinding: "workflow.external-tool.capability", | ||
| reason: "Search verified OSINT providers.", | ||
| missing: [], | ||
| tags: ["catalog", "tool-capability", "osint"], | ||
| source: "backend.workflow.tool_capabilities", | ||
| manifest: { | ||
| canvas: { node: true }, | ||
| nodeCatalog: { | ||
| authority: "backend", | ||
| origin: "tool-capability", | ||
| category: "processing", | ||
| }, | ||
| presentation: { | ||
| icon: "Search", | ||
| parameters: [ | ||
| { | ||
| name: "toolCapability", | ||
| label: "Tool binding", | ||
| type: "object", | ||
| required: true, | ||
| default: toolCapability, | ||
| }, | ||
| { | ||
| name: "toolParams", | ||
| label: "Runtime parameters", | ||
| type: "object", | ||
| default: { limit: 20 }, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| } | ||
| const capabilities = { | ||
| version: "test", | ||
| catalog: [runtimeCapability], | ||
| primitives: [], | ||
| channels: [], | ||
| notifiers: [], | ||
| triggers: [], | ||
| resources: [], | ||
| } | ||
|
|
||
| const item = getWorkflowNodeCatalog("intelligence", capabilities).find( | ||
| (candidate) => candidate.id === runtimeCapability.id, | ||
| ) | ||
| assert.ok(item) | ||
| assert.equal(item.kind, "action") | ||
| assert.equal(item.capability, "store") | ||
| assert.deepEqual(item.params.toolCapability, toolCapability) | ||
| assert.deepEqual(item.params.toolParams, { limit: 20 }) | ||
|
|
||
| const node = createWorkflowNodeFromCatalog(item, "osint-search", { x: 80, y: 120 }) | ||
| assert.deepEqual(node.params.toolCapability, toolCapability) | ||
| assert.deepEqual(node.params.toolParams, { limit: 20 }) | ||
| assert.equal(node.ui.catalogId, "tool.osint.metasearch") | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
No de-duplication of
catalog_idacross projected tools._tool_catalog_capabilities()emits one row per matching tool without checking whethercatalog_idcollides with another tool'snodeCatalog.id(or with a staticWORKFLOW_CATALOG_IDSentry). Since callers commonly build a{item.id: item}map, a collision means one capability silently disappears from the catalog rather than surfacing a config error.Consider tracking seen ids and raising/logging on collision (or filtering) so a copy-paste mistake in a future
catalog={...}block doesn't silently hide a tool node.Also applies to: 1457-1457, 1479-1479
🤖 Prompt for AI Agents