network-ai 3.1.0 → 3.1.2
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/SKILL.md +10 -3
- package/package.json +1 -1
- package/scripts/blackboard.py +40 -7
package/SKILL.md
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Multi-agent swarm orchestration for complex workflows.
|
|
4
|
-
metadata:
|
|
2
|
+
name: Network-AI
|
|
3
|
+
description: Multi-agent swarm orchestration for complex workflows. Coordinates multiple agents, decomposes tasks, manages shared state via a local blackboard file, and enforces permission walls before sensitive operations. All execution is local and sandboxed.
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw:
|
|
6
|
+
emoji: "\U0001F41D"
|
|
7
|
+
homepage: https://github.com/jovanSAPFIONEER/Network-AI
|
|
8
|
+
requires:
|
|
9
|
+
bins:
|
|
10
|
+
- python3
|
|
11
|
+
- node
|
|
5
12
|
---
|
|
6
13
|
|
|
7
14
|
# Swarm Orchestrator Skill
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "network-ai",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "AI agent orchestration framework for TypeScript/Node.js - plug-and-play multi-agent coordination with 12 frameworks (LangChain, AutoGen, CrewAI, OpenAI Assistants, LlamaIndex, Semantic Kernel, Haystack, DSPy, Agno, MCP, OpenClaw). Built-in security, swarm intelligence, and agentic workflow patterns.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/scripts/blackboard.py
CHANGED
|
@@ -307,7 +307,38 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
307
307
|
# ========================================================================
|
|
308
308
|
# ATOMIC COMMIT WORKFLOW: propose → validate → commit
|
|
309
309
|
# ========================================================================
|
|
310
|
-
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def _sanitize_change_id(change_id: str) -> str:
|
|
313
|
+
"""
|
|
314
|
+
Sanitize change_id to prevent path traversal attacks.
|
|
315
|
+
Only allows alphanumeric characters, hyphens, underscores, and dots.
|
|
316
|
+
Rejects any path separators or parent directory references.
|
|
317
|
+
"""
|
|
318
|
+
if not change_id or not isinstance(change_id, str):
|
|
319
|
+
raise ValueError("change_id must be a non-empty string")
|
|
320
|
+
# Strip whitespace
|
|
321
|
+
sanitized = change_id.strip()
|
|
322
|
+
# Reject path separators and parent directory traversal
|
|
323
|
+
if any(c in sanitized for c in ('/', '\\', '..')):
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"Invalid change_id '{change_id}': must not contain path separators or '..'"
|
|
326
|
+
)
|
|
327
|
+
# Only allow safe characters: alphanumeric, hyphen, underscore, dot
|
|
328
|
+
if not re.match(r'^[a-zA-Z0-9_\-\.]+$', sanitized):
|
|
329
|
+
raise ValueError(
|
|
330
|
+
f"Invalid change_id '{change_id}': only alphanumeric, hyphen, underscore, and dot allowed"
|
|
331
|
+
)
|
|
332
|
+
return sanitized
|
|
333
|
+
|
|
334
|
+
def _safe_pending_path(self, change_id: str, suffix: str = ".pending.json") -> Path:
|
|
335
|
+
"""Build a pending-file path and verify it stays inside pending_dir."""
|
|
336
|
+
safe_id = self._sanitize_change_id(change_id)
|
|
337
|
+
target = (self.pending_dir / f"{safe_id}{suffix}").resolve()
|
|
338
|
+
if not str(target).startswith(str(self.pending_dir.resolve())):
|
|
339
|
+
raise ValueError(f"Path traversal blocked for change_id '{change_id}'")
|
|
340
|
+
return target
|
|
341
|
+
|
|
311
342
|
def propose_change(self, change_id: str, key: str, value: Any,
|
|
312
343
|
source_agent: str = "unknown", ttl: Optional[int] = None,
|
|
313
344
|
operation: str = "write") -> dict[str, Any]:
|
|
@@ -317,7 +348,7 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
317
348
|
The change is written to a .pending file and must be validated
|
|
318
349
|
and committed by the orchestrator before it takes effect.
|
|
319
350
|
"""
|
|
320
|
-
pending_file = self.
|
|
351
|
+
pending_file = self._safe_pending_path(change_id)
|
|
321
352
|
|
|
322
353
|
# Check for duplicate change_id
|
|
323
354
|
if pending_file.exists():
|
|
@@ -365,7 +396,7 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
365
396
|
- No conflicting changes to the same key
|
|
366
397
|
- Base hash matches (data hasn't changed since proposal)
|
|
367
398
|
"""
|
|
368
|
-
pending_file = self.
|
|
399
|
+
pending_file = self._safe_pending_path(change_id)
|
|
369
400
|
|
|
370
401
|
if not pending_file.exists():
|
|
371
402
|
return {
|
|
@@ -439,7 +470,7 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
439
470
|
"""
|
|
440
471
|
Apply a validated change atomically (Step 3 of atomic commit).
|
|
441
472
|
"""
|
|
442
|
-
pending_file = self.
|
|
473
|
+
pending_file = self._safe_pending_path(change_id)
|
|
443
474
|
|
|
444
475
|
if not pending_file.exists():
|
|
445
476
|
return {
|
|
@@ -479,9 +510,10 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
479
510
|
change_set["status"] = "committed"
|
|
480
511
|
change_set["committed_at"] = datetime.now(timezone.utc).isoformat()
|
|
481
512
|
|
|
513
|
+
safe_id = self._sanitize_change_id(change_id)
|
|
482
514
|
archive_dir = self.pending_dir / "archive"
|
|
483
515
|
archive_dir.mkdir(exist_ok=True)
|
|
484
|
-
archive_file = archive_dir / f"{
|
|
516
|
+
archive_file = archive_dir / f"{safe_id}.committed.json"
|
|
485
517
|
archive_file.write_text(json.dumps(change_set, indent=2))
|
|
486
518
|
|
|
487
519
|
# Remove pending file
|
|
@@ -497,7 +529,7 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
497
529
|
|
|
498
530
|
def abort_change(self, change_id: str) -> dict[str, Any]:
|
|
499
531
|
"""Abort a pending change without applying it."""
|
|
500
|
-
pending_file = self.
|
|
532
|
+
pending_file = self._safe_pending_path(change_id)
|
|
501
533
|
|
|
502
534
|
if not pending_file.exists():
|
|
503
535
|
return {
|
|
@@ -510,9 +542,10 @@ Last Updated: {datetime.now(timezone.utc).isoformat()}
|
|
|
510
542
|
change_set["aborted_at"] = datetime.now(timezone.utc).isoformat()
|
|
511
543
|
|
|
512
544
|
# Archive the aborted change
|
|
545
|
+
safe_id = self._sanitize_change_id(change_id)
|
|
513
546
|
archive_dir = self.pending_dir / "archive"
|
|
514
547
|
archive_dir.mkdir(exist_ok=True)
|
|
515
|
-
archive_file = archive_dir / f"{
|
|
548
|
+
archive_file = archive_dir / f"{safe_id}.aborted.json"
|
|
516
549
|
archive_file.write_text(json.dumps(change_set, indent=2))
|
|
517
550
|
|
|
518
551
|
pending_file.unlink()
|