Skip to content

Commit 7730b91

Browse files
committed
feat: add native skill package runtime with explicit target-repo skill trust
1 parent 40cc2de commit 7730b91

32 files changed

Lines changed: 2729 additions & 93 deletions

agentflow/cli.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
DoctorReport,
2525
LocalToolchainReport,
2626
ShellBridgeRecommendation,
27+
build_pipeline_skill_policy_context,
2728
build_bash_login_shell_bridge_recommendation,
2829
build_local_kimi_toolchain_report,
2930
build_local_kimi_bootstrap_doctor_report,
@@ -813,21 +814,29 @@ def _doctor_report_for_path(path: str | None = None) -> tuple[object, dict[str,
813814
except typer.Exit:
814815
return report, None, None
815816
include_ok_local_checks = _include_ok_local_preflight_checks(selected_path, pipeline)
817+
pipeline_context = {"auto_preflight": _auto_smoke_preflight_metadata(selected_path, pipeline)}
818+
skill_policy_context = build_pipeline_skill_policy_context(pipeline)
819+
if skill_policy_context:
820+
pipeline_context.update(skill_policy_context)
816821
return (
817822
_augment_preflight_report(
818823
report,
819824
pipeline,
820825
include_ok_local_checks=include_ok_local_checks,
821826
),
822-
{"auto_preflight": _auto_smoke_preflight_metadata(selected_path, pipeline)},
827+
pipeline_context,
823828
pipeline,
824829
)
825830
pipeline = _load_pipeline(path)
826831
report = _preflight_base_report(path, pipeline)
827832
include_ok_local_checks = _include_ok_local_preflight_checks(path, pipeline)
833+
pipeline_context = {"auto_preflight": _auto_smoke_preflight_metadata(path, pipeline)}
834+
skill_policy_context = build_pipeline_skill_policy_context(pipeline)
835+
if skill_policy_context:
836+
pipeline_context.update(skill_policy_context)
828837
return (
829838
_augment_preflight_report(report, pipeline, include_ok_local_checks=include_ok_local_checks),
830-
{"auto_preflight": _auto_smoke_preflight_metadata(path, pipeline)},
839+
pipeline_context,
831840
pipeline,
832841
)
833842

@@ -1574,6 +1583,9 @@ def _load_pipeline_with_optional_smoke_preflight(
15741583
preflight_context = {
15751584
"auto_preflight": _auto_smoke_preflight_metadata(path or selected_path, preflight_pipeline)
15761585
}
1586+
skill_policy_context = build_pipeline_skill_policy_context(preflight_pipeline)
1587+
if skill_policy_context:
1588+
preflight_context.update(skill_policy_context)
15771589
if report.status == "failed":
15781590
_echo_doctor_report(
15791591
report,
@@ -1623,6 +1635,38 @@ def _render_shell_bridge_summary(shell_bridge: object | None) -> str:
16231635
)
16241636

16251637

1638+
def _render_skill_policy_summary_lines(pipeline: dict[str, object] | None) -> list[str]:
1639+
if not isinstance(pipeline, dict):
1640+
return []
1641+
1642+
skill_policy = pipeline.get("skill_policy")
1643+
if not isinstance(skill_policy, dict):
1644+
return []
1645+
1646+
lines: list[str] = []
1647+
owned_roots = [str(root) for root in skill_policy.get("owned_roots", []) if isinstance(root, str) and root]
1648+
if owned_roots:
1649+
lines.append(f"Pipeline skill roots: AgentFlow-owned `.agents/skills/` -> {', '.join(owned_roots)}")
1650+
1651+
target_roots = [str(root) for root in skill_policy.get("target_roots", []) if isinstance(root, str) and root]
1652+
target_suffix = f" -> {', '.join(target_roots)}" if target_roots else ""
1653+
if skill_policy.get("default_target_repo_skill_trust") is False:
1654+
lines.append(f"Pipeline target repo skills: ignored by default{target_suffix}")
1655+
1656+
trusted_nodes = [str(node_id) for node_id in skill_policy.get("trusted_nodes", []) if isinstance(node_id, str) and node_id]
1657+
if trusted_nodes:
1658+
lines.append(f"Pipeline target repo skill trust: enabled for {', '.join(trusted_nodes)}")
1659+
1660+
boundary = skill_policy.get("repo_instructions_boundary")
1661+
if isinstance(boundary, str) and boundary.strip():
1662+
lines.append(
1663+
"Pipeline repo instructions: separate from skill trust; use `repo_instructions_mode` for `AGENTS.md`, "
1664+
"`CLAUDE.md`, and related instruction files."
1665+
)
1666+
1667+
return lines
1668+
1669+
16261670
def _doctor_check_summary_suffix(check: object) -> str:
16271671
if getattr(check, "name", None) != "bash_login_startup":
16281672
return ""
@@ -1672,6 +1716,7 @@ def _render_doctor_summary(
16721716
rendered_matches = [match for match in matches if isinstance(match, str) and match]
16731717
if rendered_matches:
16741718
lines.append(f"{auto_preflight_label} matches: {', '.join(rendered_matches)}")
1719+
lines.extend(_render_skill_policy_summary_lines(pipeline))
16751720
if include_shell_bridge:
16761721
lines.append(_render_shell_bridge_summary(shell_bridge))
16771722
return "\n".join(lines)

agentflow/context.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,11 @@ def render_node_prompt(
229229
current_tick_started_at=current_tick_started_at,
230230
)
231231
prompt = render_template(node.prompt, context)
232-
skill_prelude = compile_skill_prelude(node.skills, pipeline.working_path)
232+
skill_prelude = compile_skill_prelude(
233+
node.skills,
234+
pipeline.working_path,
235+
target_skill_policy=node.target_skill_policy,
236+
)
233237
if skill_prelude:
234238
return f"Selected skills:\n{skill_prelude}\n\nTask:\n{prompt}"
235239
return prompt

agentflow/doctor.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from pydantic import ValidationError
1515

16+
from agentflow import skill_roots
1617
from agentflow.env import merge_env_layers
1718
from agentflow.local_shell import (
1819
_bash_login_startup_has_direct_agentflow_bootstrap,
@@ -25,6 +26,7 @@
2526
)
2627
from agentflow.prepared import PreparedExecution, build_execution_paths
2728
from agentflow.runners.local import LocalRunner
29+
from agentflow.skill_packages import is_package_skill_ref
2830
from agentflow.specs import AgentKind, LocalTarget, provider_uses_kimi_anthropic_auth, resolve_provider
2931
from agentflow.utils import looks_sensitive_key
3032

@@ -437,6 +439,58 @@ def _dict_env(env: object) -> dict[str, str]:
437439
}
438440

439441

442+
def build_pipeline_skill_policy_context(pipeline: object | None) -> dict[str, object] | None:
443+
if pipeline is None:
444+
return None
445+
446+
nodes = getattr(pipeline, "nodes", None) or []
447+
node_entries: list[dict[str, object]] = []
448+
trusted_nodes: list[str] = []
449+
for node in nodes:
450+
raw_skills = _object_value(node, "skills", ()) or ()
451+
package_skills = [
452+
str(skill)
453+
for skill in raw_skills
454+
if isinstance(skill, str) and is_package_skill_ref(skill)
455+
]
456+
target_skill_policy = _status_value(_object_value(node, "target_skill_policy", "none")) or "none"
457+
if not package_skills and target_skill_policy == "none":
458+
continue
459+
460+
node_id = str(_object_value(node, "id", "node"))
461+
repo_instructions_mode = _status_value(_object_value(node, "repo_instructions_mode", "inherit")) or "inherit"
462+
node_entries.append(
463+
{
464+
"node_id": node_id,
465+
"repo_instructions_mode": repo_instructions_mode,
466+
"target_skill_policy": target_skill_policy,
467+
"package_skills": package_skills,
468+
}
469+
)
470+
if target_skill_policy == "inherit_all":
471+
trusted_nodes.append(node_id)
472+
473+
if not node_entries:
474+
return None
475+
476+
working_path = _object_value(pipeline, "working_path")
477+
owned_roots = [str(Path(root).expanduser().resolve()) for root in skill_roots.owned_skill_package_roots()]
478+
target_roots = [str(Path(root).expanduser().resolve()) for root in skill_roots.target_repo_skill_package_roots(working_path)]
479+
return {
480+
"skill_policy": {
481+
"owned_roots": owned_roots,
482+
"target_roots": target_roots,
483+
"default_target_repo_skill_trust": False,
484+
"repo_instructions_boundary": (
485+
"Repo-local instructions are controlled separately by `repo_instructions_mode`; "
486+
"target repo skill trust only affects `.agents/skills/` packages."
487+
),
488+
"trusted_nodes": trusted_nodes,
489+
"nodes": node_entries,
490+
}
491+
}
492+
493+
440494
def _toolchain_ambient_base_urls() -> dict[str, str]:
441495
ambient: dict[str, str] = {}
442496
for key in _TOOLCHAIN_AMBIENT_BASE_URL_VARS:

0 commit comments

Comments
 (0)