Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1f9ef47
feat: add alert_dedup workflow for security alert deduplication
duguwanglong May 8, 2026
1752b5b
refactor: align alert_dedup workflow with full LogProcessPipeline (4 …
duguwanglong May 8, 2026
746bb83
fix(alert_dedup): align filter logic with aisoc_mini LogFilter and pa…
duguwanglong May 8, 2026
f8f0c83
feat(alert_dedup): add explicit branch nodes for log-type and filter-…
duguwanglong May 8, 2026
44fdca5
refactor(workflow): rename alert_dedup → network_alert_dedup, simplif…
duguwanglong May 8, 2026
edd9d63
chore(workflow): remove aisoc_mini references from network_alert_dedup
duguwanglong May 8, 2026
7b2cbd0
feat(workflow): add branch_has_alerts after filter_logs in network_al…
duguwanglong May 8, 2026
4f59e98
refactor(workflow): rename network_alert_dedup → http_alert_dedup
duguwanglong May 8, 2026
248aa19
refactor(workflow): remove branch_has_alerts and dedup_empty from htt…
duguwanglong May 8, 2026
7ef901b
feat(workflow): replace brute-force Jaccard with MinHash LSH in dedup…
duguwanglong May 8, 2026
89019d3
refactor(workflow): English comments and expose _lsh_cluster_id in de…
duguwanglong May 8, 2026
ce4a920
feat(workflow): persist LSH state to disk in dedup_logs
duguwanglong May 8, 2026
df2f372
fix(workflow): harden LSH state persistence in dedup_logs
duguwanglong May 8, 2026
02f67c7
fix(workflow): cross-platform file lock + cleaner stats in dedup_logs
duguwanglong May 8, 2026
56d3478
fix(run_workflow): handle JSON-encoded string path and bad dict input…
duguwanglong May 8, 2026
2580cef
fix(run_workflow): split non-dict json.loads branches with clear per-…
duguwanglong May 8, 2026
6322b36
fix(run_workflow): merge conflict — keep type branches, add workflow …
duguwanglong May 9, 2026
da58f7b
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/alert-d…
duguwanglong May 9, 2026
6265dd1
fix(workflow): re-apply workspace path for LSH state (lost in dev merge)
duguwanglong May 9, 2026
9930b72
feat(workflow): record invocation stats for published-service invoke …
duguwanglong May 9, 2026
276beab
feat(workflow): add alert dedup-triage pipeline and harden LSH eviction
duguwanglong May 9, 2026
5b86c29
feat(workflow): add syslog ingestion trigger and integration tab
duguwanglong May 9, 2026
bc5e0f9
refactor(ingest): rename flocks/syslog → flocks/ingest/syslog
duguwanglong May 9, 2026
18e6521
feat(alert_dedup_triage): support syslog real-time input mode
duguwanglong May 10, 2026
a2ea64a
refactor(alert_dedup_triage): embed sub-workflow calls via engine, re…
duguwanglong May 10, 2026
3f49f2b
style(alert_dedup_triage): translate all comments and UI strings to E…
duguwanglong May 10, 2026
ce8ef77
fix(generate_summary): remove duplicate '(cached)' label in summary t…
duguwanglong May 10, 2026
af6d142
feat(alert_dedup_triage): skip summary in syslog mode, generate full …
duguwanglong May 10, 2026
1f94e80
fix(ingest/syslog): persist execution records and stats for syslog-tr…
duguwanglong May 10, 2026
db1d260
feat(workflows): support mixed TDP/Skyeye batches and flat-format TDP…
duguwanglong May 10, 2026
2b0fd1b
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/alert-d…
duguwanglong May 12, 2026
1b5e36a
feat(ingest/workflow): add stream_alert_dedup workflow and iso3164 sy…
duguwanglong May 12, 2026
97c72fc
feat(workflows): add tdp_alert_pull_dedup workflow for TDP API polling
duguwanglong May 12, 2026
4a73d7b
fix(windows-installer): require elevation for installer shortcuts
May 12, 2026
a8b4156
feat(windows): bundle python-build-standalone runtime in installer st…
xiami762 May 12, 2026
10498b8
fix(channel_message): attach Bearer API token on local HTTP send
duguwanglong May 12, 2026
c13ef59
refactor(channel_message): reuse API_TOKEN_SECRET_ID and clarify fall…
duguwanglong May 12, 2026
b33ba18
docs: add contributing guide (#257)
xiami762 May 12, 2026
2b312bc
feat(server,webui): phased startup, route timing, session/tools UX (#…
xiami762 May 12, 2026
2415272
chore: bump package version to v2026.5.12
May 12, 2026
e211501
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/tdp-ale…
duguwanglong May 13, 2026
21ca0d7
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/tdp-ale…
duguwanglong May 13, 2026
bba801b
docs(skills): web2cli flow, capture path, and browser experience in s…
xiami762 May 13, 2026
a1a324d
fix(workflow,plugin): stop watcher reload loop and harden execution path
duguwanglong May 14, 2026
785147d
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/tdp-ale…
duguwanglong May 14, 2026
9c53a56
chore(workflows): drop legacy dedup workflows and rewrite tdp_alert_t…
duguwanglong May 14, 2026
7a66670
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/tdp-ale…
duguwanglong May 14, 2026
a0fcab4
fix(syslog/watcher): address PR-267 review — backpressure, atomic-sav…
duguwanglong May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions flocks/agent/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,27 @@ def unregister(cls, name: str) -> bool:
# ---------------------------------------------------------------------------


def _agent_event_should_reload(event: object) -> bool:
"""Return True if a watchdog event should invalidate the agent cache.

Mirrors ``flocks.tool.registry._tool_event_should_reload``: atomic-save
editors surface the real target via ``dest_path``, so we inspect both
endpoints before deciding to skip.
"""
candidates = []
src = getattr(event, "src_path", "") or ""
if src:
candidates.append(src)
dest = getattr(event, "dest_path", "") or ""
if dest:
candidates.append(dest)
for path in candidates:
fname = os.path.basename(path)
if fname == "agent.yaml" or path.endswith(".md"):
return True
return False


class AgentFileWatcher:
"""Watch plugin agent directories and auto-invalidate the Agent cache on change.

Expand Down Expand Up @@ -621,13 +642,20 @@ def start(self) -> None:

watcher = self

# Only react to actual content-mutation events. Without this guard the
# ``opened``/``closed``/``closed_no_write`` events that watchdog emits
# whenever any code (including the agent loader itself) reads
# ``agent.yaml`` / ``*.md`` would re-trigger cache invalidation on every
# access, causing a self-sustaining reload loop.
_RELOAD_EVENT_TYPES = frozenset({"modified", "created", "deleted", "moved"})

class _Handler(FileSystemEventHandler):
def on_any_event(self, event: FileSystemEvent) -> None:
if event.is_directory:
return
src = getattr(event, "src_path", "") or ""
fname = os.path.basename(src)
if fname == "agent.yaml" or src.endswith(".md"):
if getattr(event, "event_type", "") not in _RELOAD_EVENT_TYPES:
return
if _agent_event_should_reload(event):
watcher._schedule_invalidate()

handler = _Handler()
Expand Down
6 changes: 5 additions & 1 deletion flocks/agent/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ def normalize_declared_tool_names(
matches = [raw_name] if raw_name in available else []

if not matches:
log.warn("agent.toolset.tool_missing", {"tool": raw_name})
# Built-in agent definitions (librarian, metis, …) declare optional
# tools such as ``lsp_*`` / ``ast_grep_search`` that ship in separate
# binaries; they are gracefully skipped when not installed. Treat
# this as informational only to avoid flooding operational logs.
log.debug("agent.toolset.tool_missing", {"tool": raw_name})
continue

for match in matches:
Expand Down
2 changes: 1 addition & 1 deletion flocks/command/command_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def discover_commands() -> Dict[str, CommandInfo]:

try:
from flocks.utils.compat import get_flocks_config_dir
opencode_global_dir = str(get_flocks_config_dir(binary="opencode") / "command")
flocks_global_dir = str(get_flocks_config_dir(binary="opencode") / "command")
sources.append(("flocks-global", flocks_global_dir, "**/*.md"))
except Exception as e:
log.warn("command.flocks_dir.error", {"error": str(e)})
Expand Down
Empty file added flocks/ingest/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions flocks/ingest/syslog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Syslog ingestion for workflow triggers (UDP/TCP listeners)."""

from flocks.ingest.syslog.constants import WORKFLOW_SYSLOG_CONFIG_PREFIX
from flocks.ingest.syslog.manager import SyslogManager, default_manager

__all__ = ["SyslogManager", "default_manager", "WORKFLOW_SYSLOG_CONFIG_PREFIX"]
3 changes: 3 additions & 0 deletions flocks/ingest/syslog/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Storage key prefix for per-workflow syslog config (must match server routes)."""

WORKFLOW_SYSLOG_CONFIG_PREFIX = "workflow_syslog_config/"
128 changes: 128 additions & 0 deletions flocks/ingest/syslog/listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Asyncio UDP/TCP syslog listeners."""

from __future__ import annotations

import asyncio
from typing import Awaitable, Callable, Union

from flocks.ingest.syslog.parser import parse_syslog

OnSyslogMessage = Callable[[dict], Union[None, Awaitable[None]]]


class SyslogUDPProtocol(asyncio.DatagramProtocol):
"""Receive syslog datagrams and invoke async callback with parsed dict.

The *on_message* callback is expected to be non-blocking (e.g. a queue
put_nowait). This protocol deliberately does NOT create unbounded asyncio
tasks on every datagram — the caller owns concurrency control.
"""

def __init__(
self,
on_message: OnSyslogMessage,
format_hint: str,
) -> None:
self._on_message = on_message
self._format_hint = format_hint

def datagram_received(self, data: bytes, _addr) -> None: # noqa: ANN001
text = data.decode("utf-8", errors="replace")
parsed = parse_syslog(text, self._format_hint)
try:
res = self._on_message(parsed)
# If the callback returns a coroutine (legacy path), schedule it
# but only once — do NOT create_task without bound.
if asyncio.iscoroutine(res):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return
loop.create_task(self._safe_await(res))
except Exception:
pass

@staticmethod
async def _safe_await(coro) -> None: # noqa: ANN001
try:
await coro
except Exception:
pass


async def run_udp_syslog_server(
host: str,
port: int,
format_hint: str,
on_message: OnSyslogMessage,
*,
abort_event: asyncio.Event,
) -> None:
loop = asyncio.get_running_loop()
transport, _protocol = await loop.create_datagram_endpoint(
lambda: SyslogUDPProtocol(on_message, format_hint),
local_addr=(host, port),
)
try:
await abort_event.wait()
finally:
transport.close()


async def _handle_tcp_client(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
format_hint: str,
on_message: OnSyslogMessage,
) -> None:
try:
while True:
line = await reader.readline()
if not line:
break
text = line.decode("utf-8", errors="replace").strip()
if not text:
continue
parsed = parse_syslog(text, format_hint)
try:
res = on_message(parsed)
if asyncio.iscoroutine(res):
await res
except Exception:
pass
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass


async def run_tcp_syslog_server(
host: str,
port: int,
format_hint: str,
on_message: OnSyslogMessage,
*,
abort_event: asyncio.Event,
) -> None:
async def handle_client(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
) -> None:
await _handle_tcp_client(reader, writer, format_hint, on_message)

server = await asyncio.start_server(handle_client, host, port)
serve_task: asyncio.Task[None] | None = None
try:
serve_task = asyncio.create_task(server.serve_forever())
await abort_event.wait()
finally:
if serve_task and not serve_task.done():
serve_task.cancel()
try:
await serve_task
except asyncio.CancelledError:
pass
server.close()
await server.wait_closed()
Loading