Skip to content

[nightly][agent-sec-core] daemon rejects SkillFS v1 notify protocol (skillDir/skillName) — auto-notify silently fails, skills stuck at unmanaged #1969

Description

@zhangtaibo

Component

agent-sec-core

Bug Description

The agent-sec-core daemon (v0.9.0) rejects SkillFS change notifications with bad_request: params contains unknown fields: skillDir, skillName. This happens because the installed SkillFS binary (v0.3.2) sends the v1 notify protocol with field names skillDir/skillName, while the daemon's parse_skillfs_change handler only accepts v2 field names canonicalSkillDir/skillId. The rejection causes SkillFS auto-notify to silently fail — skills written through the FUSE mount are never processed by the daemon, so latestStatus stays unmanaged instead of transitioning to pass or deny.

The SkillFS source code (v0.4.0, not yet released as RPM) already uses the v2 protocol, but the deployed v0.3.2 binary still sends v1. The daemon should accept v1 notifications for backward compatibility with deployed SkillFS versions.

该问题由 AgenticOS Nightly 自动化测试在 2026-07-29 nightly(nightly-20260729-020133,anolisa main HEAD)如实上报后定位。

Steps to Reproduce

# 1. Start the agent-sec-core daemon
systemctl --user start agent-sec-core.service

# 2. Mount SkillFS with --notify-socket pointing to the daemon socket
mkdir -p /run/skillfs-test/target /run/skillfs-test/backing /run/skillfs-test/report
skillfs mount /run/skillfs-test/target /run/skillfs-test/target \
  --foreground --security --activation-mode file \
  --notify-socket /run/user/0/agent-sec-core/daemon.sock \
  --activation-reload-mode poll \
  --ledger-backing-root /run/skillfs-test/backing \
  --config /run/skillfs-test/report/install.toml \
  --activation-events-log /run/skillfs-test/report/events.jsonl \
  --audit-log /run/skillfs-test/report/audit.jsonl \
  --trusted-writer-exe /usr/bin/python3.11 \
  --log-file /run/skillfs-test/report/skillfs.log &

# 3. Write a SKILL.md through the FUSE mount
mkdir -p /run/skillfs-test/target/test-skill
cat > /run/skillfs-test/target/test-skill/SKILL.md <<'EOF'
---
name: test-skill
description: test fixture
---
test
EOF

# 4. Wait 2s for the debounced notification, then check the daemon log
sleep 2
tail -5 /var/log/agent-sec/daemon.jsonl | python3 -c "
import sys, json
for line in sys.stdin:
    d = json.loads(line.strip())
    data = d.get('data', {})
    if data.get('method') == 'skill_ledger.skillfs_notify_change':
        print(f\"ok={data.get('ok')}, error_code={data.get('error_code')}\")"
# Actual output: ok=False, error_code=bad_request

# 5. Check the SkillFS log for the rejection
grep "rejected\|notify.*failed" /run/skillfs-test/report/skillfs.log
# Actual: error=notify: rejected: {"ok":false,"error":{"code":"bad_request","message":"params contains unknown fields: skillDir, skillName"}}

# 6. Poll the skill ledger — status is stuck at unmanaged
agent-sec-cli skill-ledger show /run/skillfs-test/backing/test-skill
# Actual: {"latestStatus":"unmanaged","managed":false,"manageabilityReason":"canonical skill root is not configured in managedSkillDirs"}

Expected Behavior

The daemon should accept SkillFS v1 protocol notifications (with skillDir/skillName fields) and normalize them to v2 internally. After the notification is accepted, the daemon should process the skill change and latestStatus should transition to pass or deny (not stuck at unmanaged).

Suggested Fix

--- a/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/daemon/handlers/skill_ledger.py
+++ b/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/daemon/handlers/skill_ledger.py
@@ -35,6 +35,12 @@ _NOTIFY_V2_PARAM_KEYS = frozenset(
         "paths",
     }
 )
+# SkillFS v0.3.x sends v1 protocol with skillDir/skillName instead of
+# canonicalSkillDir/skillId.  Map v1 fields to v2 for backward compat.
+_NOTIFY_V1_TO_V2_FIELD_MAP = {
+    "skillDir": "canonicalSkillDir",
+    "skillName": "skillId",
+}
@@ -85,12 +91,22 @@ def skillfs_notify_change_handler(
 def parse_skillfs_change(params: dict[str, Any]) -> SkillFsChange:
-    """Validate daemon request params for a SkillFS change notification."""
-    _validate_notify_v2_param_keys(params)
+    """Validate daemon request params for a SkillFS change notification.
+
+    Accepts both v2 (canonicalSkillDir/skillId) and v1 (skillDir/skillName)
+    protocol fields.  SkillFS v0.3.x sends v1; v0.4+ sends v2.
+    """
     schema_version = params.get("schemaVersion")
-    if schema_version != SCHEMA_VERSION:
-        raise BadRequestError("params.schemaVersion must be 2")
+    # Backward-compat: normalize v1 fields to v2 before strict validation.
+    if schema_version == 1 or "skillDir" in params:
+        params = _normalize_v1_params(params)
+    _validate_notify_v2_param_keys(params)
+
+    if schema_version not in (1, 2):
+        raise BadRequestError(
+            f"params.schemaVersion must be 1 or 2, got {schema_version!r}"
+        )
 
     canonical_skill_dir = _validate_canonical_skill_dir(params.get("canonicalSkillDir"))
     skill_id = _validate_skill_id(params.get("skillId"))
@@ -121,6 +137,16 @@ def _validate_notify_v2_param_keys(params: dict[str, Any]) -> None:
         raise BadRequestError(f"params is missing required fields: {names}")
 
+def _normalize_v1_params(params: dict[str, Any]) -> dict[str, Any]:
+    """Normalize SkillFS v1 notify params to v2 field names."""
+    normalized = dict(params)
+    for v1_key, v2_key in _NOTIFY_V1_TO_V2_FIELD_MAP.items():
+        if v1_key in normalized:
+            normalized[v2_key] = normalized.pop(v1_key)
+    normalized["schemaVersion"] = SCHEMA_VERSION
+    return normalized
+
 def _validate_canonical_skill_dir(value: Any) -> Path:

已在 ECS 验证:bash scripts/rpm-build.sh agent-sec-core → exit 0,产出 agent-sec-core-0.9.0-1.alnx4.x86_64.rpm。修复后 test_skillfs_auto_notify_safe_becomes_pass 测试通过 (23.83s)。

Environment

  • ALinux 4 ECS(cn-hongkong, 47.239.61.17)
  • anolisa main:HEAD
  • agent-sec-core 0.9.0, skillfs 0.3.2

Relevant Log Output

SkillFS log:
  WARN skillfs_fuse::security::notify: notify: failed to send change notification; current activation mapping unchanged skill=safe-skill-48c6b4 error=notify: rejected: {"ok":false,"error":{"code":"bad_request","message":"params contains unknown fields: skillDir, skillName"}}

Daemon log:
  {"event":"daemon_request_completed","data":{"method":"skill_ledger.skillfs_notify_change","ok":false,"exit_code":1,"error_code":"bad_request","bytes_in":279,"bytes_out":259}}

SkillFS events log:
  {"schemaVersion":1,"skillDir":"/run/skillfs-journey-XXX/skill-backing/safe-skill-XXX","skillName":"safe-skill-XXX","eventKind":"write","paths":["SKILL.md"]}

Additional Context

由 AgenticOS Nightly([nightly] 前缀)发现并定位 + 自动提交修复 PR。

Metadata

Metadata

Labels

component:sec-coresrc/agent-sec-core/type:bugA confirmed defect or incorrect behavior

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions