From 9995cefc025d64304517bcf2ac3c9e02bec1ed24 Mon Sep 17 00:00:00 2001 From: AIGC Director Kit contributors Date: Wed, 12 Aug 2026 17:11:52 +0800 Subject: [PATCH] validate local Skill workflow integration --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 7 + README.md | 21 ++- docs/local-skill-adapter.md | 20 ++- docs/roadmap.md | 2 + docs/skill-workflow-integration.md | 26 +++- pyproject.toml | 2 +- src/aigc_director_kit/__init__.py | 3 + src/aigc_director_kit/_version.py | 2 +- src/aigc_director_kit/cli.py | 30 ++++ src/aigc_director_kit/integration.py | 194 ++++++++++++++++++++++++++ src/aigc_director_kit/verification.py | 17 ++- tests/test_cli.py | 15 ++ tests/test_integration.py | 136 ++++++++++++++++++ tests/test_verification.py | 2 +- 15 files changed, 464 insertions(+), 14 deletions(-) create mode 100644 src/aigc_director_kit/integration.py create mode 100644 tests/test_integration.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2192d85..459bf46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: run: | python -m aigc_director_kit validate-local-skill-adapter examples/local_skill_adapter_case.json --json python -m aigc_director_kit validate-workflow examples/skill_workflow_case.json --library examples/action_library.json --json + python -m aigc_director_kit validate-skill-integration examples/local_skill_adapter_case.json examples/skill_workflow_case.json --library examples/action_library.json --json python -m aigc_director_kit validate-prompt-pack examples/prompt_pack_case.json --plan examples/one_take_previs_case.json --json python -m aigc_director_kit validate-qc-report examples/qc_report_unverified_case.json --json python -m aigc_director_kit build-runtime-handoff examples/skill_workflow_case.json --library examples/action_library.json --adapter optional-runtime-adapter --json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9900d37..fdf5570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CineThread Changelog +## [0.2.4] - 2026-08-12 + +- Added `validate-skill-integration` to cross-check a metadata adapter against a project workflow. +- Added stage ownership, output-contract, evidence-semantics, required-producer, and action-producer validation. +- Added high-confidence public-safety checks for credential fields and values, email addresses, and absolute local or network paths while keeping manual privacy review mandatory. +- Added regression tests, documentation, CI coverage, and one-command public verification coverage for the integrated handoff. + ## [0.2.3] - 2026-08-12 - Added `aigc-director-local-skill-adapter`, a strict metadata-only contract for describing the interface between local Skills and CineThread. diff --git a/README.md b/README.md index 0c84ec8..eb1dabf 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ py -3 -m aigc_director_kit verify-examples --json --output cinethread-verify.jso python3 -m aigc_director_kit verify-examples --json --output cinethread-verify.json ``` -It checks every public plan, workflow, prompt-pack, QC-boundary, action, and -runtime-handoff example, and returns a privacy-safe report with relative paths -only. It does not render or generate media. For a real bug report, review the +It checks every public plan, adapter/workflow integration, prompt-pack, +QC-boundary, action, and runtime-handoff example, and returns a privacy-safe +report with relative paths only. It does not render or generate media. For a real bug report, review the output first and use the [60-second trial guide](docs/try-it.md) or the [Verified run feedback](https://github.com/cyou1314/cinethread/issues/new/choose) form. Windows users who do not know which Python command to use can instead double-click @@ -61,6 +61,21 @@ python -m aigc_director_kit validate-local-skill-adapter ` --json ``` +Validate that the reusable adapter and one project workflow actually agree: + +```powershell +python -m aigc_director_kit validate-skill-integration ` + examples/local_skill_adapter_case.json ` + examples/skill_workflow_case.json ` + --library examples/action_library.json ` + --json +``` + +This cross-check rejects undeclared workflow stages, mismatched Skill labels, +output contracts or evidence semantics, action requests assigned to the wrong +producer, and high-confidence leaks in both files such as credentials, emails, and +absolute local paths. A passing result still requires manual privacy review. + To create one packet for an optional runtime adapter: ```powershell diff --git a/docs/local-skill-adapter.md b/docs/local-skill-adapter.md index 40add47..f226b7a 100644 --- a/docs/local-skill-adapter.md +++ b/docs/local-skill-adapter.md @@ -40,6 +40,22 @@ python -m aigc_director_kit validate-local-skill-adapter ` The example maps local stage labels to existing CineThread contracts without shipping any Skill instructions. It is also included in `verify-examples`. +To prove that a project workflow uses this adapter consistently, run the +cross-file validator: + +```powershell +python -m aigc_director_kit validate-skill-integration ` + examples/local_skill_adapter_case.json ` + examples/skill_workflow_case.json ` + --library examples/action_library.json ` + --json +``` + +The workflow may use a subset of adapter stages. Every used stage must match +on id, Skill label, output contract, and evidence semantics. The validator also +checks that action requests come from an action-request producer and scans both +files for high-confidence credential, email, and local-path leaks. + ## Required public fields | Field | Meaning | Safe to publish | @@ -55,8 +71,8 @@ The manifest fixes `visibility` to `public-metadata-only` and credentials or local paths in string values are rejected as a guard against accidental publication. -That guard is structural, not a guarantee that prose is harmless. Review every -manifest before publishing. +Those guards are structural and high-confidence, not a guarantee that prose is +harmless or anonymous. Review every manifest and workflow before publishing. ## Keep these out diff --git a/docs/roadmap.md b/docs/roadmap.md index 936febf..3d2640c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,6 +8,8 @@ compiled action requests without launching Blender. - [x] A metadata-only local Skill adapter can document public integration contracts without publishing local implementation details. +- [x] Adapter/workflow cross-validation checks stage ownership, output and + evidence compatibility, action-producer routing, and high-confidence leaks. - [x] The Windows example launcher accepts `AIGC_DIRECTOR_PYTHON` and checks common Python install locations. - [ ] Add an optional Blender adapter that consumes the handoff and records diff --git a/docs/skill-workflow-integration.md b/docs/skill-workflow-integration.md index bf8f261..348b101 100644 --- a/docs/skill-workflow-integration.md +++ b/docs/skill-workflow-integration.md @@ -56,6 +56,21 @@ metadata-only field set rejects prompt bodies, paths, assets, and credentials. The separate [`local-skill-adapter.md`](local-skill-adapter.md) guide explains what may safely appear in a public manifest. +Cross-check the adapter and project workflow together before publication: + +```powershell +python -m aigc_director_kit validate-skill-integration ` + examples/local_skill_adapter_case.json ` + examples/skill_workflow_case.json ` + --library examples/action_library.json ` + --json +``` + +This requires every used workflow stage to match the adapter's id, Skill label, +output contract, and evidence semantics. It verifies required producers and +high-confidence public-safety patterns, but it cannot determine whether all +free-form prose is anonymous. Manual privacy review remains required. + ## Run the public handoff Without an action catalog, the workflow is checked structurally and action @@ -130,11 +145,14 @@ python -m aigc_director_kit validate-qc-report ` 3. Embed a versioned `aigc-director-shot-plan` in `shot_plan`. 4. Put natural-language action intent in `action_requests[]` with a shot and stage reference. -5. Run `validate-workflow --library ...` before handing the packet to an - optional runtime adapter. -6. Run `build-runtime-handoff ... --adapter ...` when the next runtime needs a +5. Run `validate-workflow --library ...`, then + `validate-skill-integration --library ...` before + publication or runtime handoff. +6. Manually review the final adapter and workflow for private prose or asset + identities that automated checks cannot infer. +7. Run `build-runtime-handoff ... --adapter ...` when the next runtime needs a single dry-run packet. -7. Add a public example and a regression test when the contract changes. +8. Add a public example and a regression test when the contract changes. Keep private source paths, credentials, cookies, private character assets, unlicensed motion data, and generated media out of the packet. Mark inferred diff --git a/pyproject.toml b/pyproject.toml index 651dbe1..d8d43ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aigc-director-kit" -version = "0.2.3" +version = "0.2.4" description = "CineThread: deterministic creative handoffs for AI animation, storyboards, and previs." readme = "README.md" requires-python = ">=3.10" diff --git a/src/aigc_director_kit/__init__.py b/src/aigc_director_kit/__init__.py index c29f316..14da4d8 100644 --- a/src/aigc_director_kit/__init__.py +++ b/src/aigc_director_kit/__init__.py @@ -4,6 +4,7 @@ from .adapter import validate_local_skill_adapter, validate_local_skill_adapter_file from .actions import compile_action_request, list_actions, load_action_library from .contract import ValidationResult, load_plan, validate_plan, validate_plan_file +from .integration import validate_skill_integration, validate_skill_integration_files from .prompt import validate_prompt_pack, validate_prompt_pack_file from .qc import validate_qc_report, validate_qc_report_file from .runtime import build_runtime_handoff, build_runtime_handoff_file @@ -21,6 +22,8 @@ "run_public_example_verification", "validate_local_skill_adapter", "validate_local_skill_adapter_file", + "validate_skill_integration", + "validate_skill_integration_files", "validate_plan", "validate_plan_file", "validate_prompt_pack", diff --git a/src/aigc_director_kit/_version.py b/src/aigc_director_kit/_version.py index d56736a..4f7d304 100644 --- a/src/aigc_director_kit/_version.py +++ b/src/aigc_director_kit/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the public package version.""" -__version__ = "0.2.3" +__version__ = "0.2.4" diff --git a/src/aigc_director_kit/cli.py b/src/aigc_director_kit/cli.py index c6fb05e..747878d 100644 --- a/src/aigc_director_kit/cli.py +++ b/src/aigc_director_kit/cli.py @@ -12,6 +12,7 @@ from .adapter import validate_local_skill_adapter_file from .actions import compile_action_request, list_actions, load_action_library from .contract import validate_plan_file +from .integration import validate_skill_integration_files from .prompt import validate_prompt_pack_file from .qc import validate_qc_report_file from .runtime import build_runtime_handoff_file @@ -47,6 +48,15 @@ def _build_parser() -> argparse.ArgumentParser: adapter.add_argument("path", type=Path) adapter.add_argument("--json", action="store_true", dest="as_json") + integration = subparsers.add_parser( + "validate-skill-integration", + help="Cross-check a public local-Skill adapter and project workflow.", + ) + integration.add_argument("adapter", type=Path) + integration.add_argument("workflow", type=Path) + integration.add_argument("--library", type=Path) + integration.add_argument("--json", action="store_true", dest="as_json") + prompt = subparsers.add_parser( "validate-prompt-pack", help="Validate a reusable prompt pack, optionally against a shot plan.", @@ -158,6 +168,26 @@ def main(argv: list[str] | None = None) -> int: print(f"warning: {warning}") return 0 if result.valid else 2 + if args.command == "validate-skill-integration": + result = validate_skill_integration_files( + args.adapter, + args.workflow, + args.library, + ) + if args.as_json: + _dump(result.as_dict()) + else: + print(f"valid: {'yes' if result.valid else 'no'}") + print(f"matched_stages: {result.summary.get('matched_stage_count', 0)}") + print(f"compiled_actions: {result.summary.get('compiled_action_count', 0)}") + print(f"public_safety_issues: {result.summary.get('public_safety_issue_count', 0)}") + print("manual_privacy_review_required: yes") + for error in result.errors: + print(f"error: {error}") + for warning in result.warnings: + print(f"warning: {warning}") + return 0 if result.valid else 2 + if args.command == "validate-prompt-pack": result = validate_prompt_pack_file(args.path, args.plan) if args.as_json: diff --git a/src/aigc_director_kit/integration.py b/src/aigc_director_kit/integration.py new file mode 100644 index 0000000..e86875c --- /dev/null +++ b/src/aigc_director_kit/integration.py @@ -0,0 +1,194 @@ +"""Cross-validate a local Skill adapter against a project workflow packet. + +This module checks that the reusable metadata adapter and the per-project +workflow agree on stage identity, Skill labels, output contracts, and evidence +semantics. It also applies high-confidence public-safety checks without +executing Skills, runtimes, generation, or media inspection. +""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any + +from .actions import load_action_library +from .adapter import validate_local_skill_adapter +from .contract import ValidationResult, load_json +from .prompt import PROMPT_CONTRACT +from .qc import QC_CONTRACT +from .workflow import WORKFLOW_CONTRACT, validate_workflow + + +SHOT_PLAN_CONTRACT = "aigc-director-shot-plan" +ACTION_REQUEST_CONTRACT = "aigc-director-action-request" +SENSITIVE_KEYS = frozenset( + { + "api_key", + "access_token", + "refresh_token", + "auth_token", + "token", + "password", + "passwd", + "secret", + "client_secret", + "cookie", + "authorization", + "credential", + "credentials", + } +) +LOCAL_PATH_PATTERN = re.compile( + r"(?i)(?:file://|\b[a-z]:[\\/]|\\\\[^\\\s]+[\\/]|/(?:home|users|private|tmp|var/tmp)/)" +) +CREDENTIAL_VALUE_PATTERN = re.compile( + r"(?i)(?:\bbearer\s+[a-z0-9._~+/=-]{8,}|\bgh[pousr]_[a-z0-9_]{20,}\b|\bsk-[a-z0-9_-]{20,}\b)" +) +EMAIL_PATTERN = re.compile(r"(?i)\b[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+\b") + + +def _normalized_key(value: object) -> str: + return re.sub(r"[^a-z0-9]+", "_", str(value).lower()).strip("_") + + +def _audit_public_value(value: Any, label: str, errors: list[str]) -> None: + """Find high-confidence leaks without echoing the sensitive value.""" + + if isinstance(value, dict): + for key, child in value.items(): + child_label = f"{label}.{key}" + if _normalized_key(key) in SENSITIVE_KEYS: + errors.append( + f"public safety: {child_label} is a credential-bearing field; remove it." + ) + _audit_public_value(child, child_label, errors) + return + if isinstance(value, list): + for index, child in enumerate(value): + _audit_public_value(child, f"{label}[{index}]", errors) + return + if not isinstance(value, str): + return + if LOCAL_PATH_PATTERN.search(value): + errors.append(f"public safety: {label} contains a local or network path; replace it with a public label.") + if CREDENTIAL_VALUE_PATTERN.search(value): + errors.append(f"public safety: {label} contains a credential-like value; remove it.") + if EMAIL_PATTERN.search(value): + errors.append(f"public safety: {label} contains an email address; replace it with a public alias.") + + +def validate_skill_integration( + adapter: Any, + workflow: Any, + action_library: dict[str, Any] | None = None, +) -> ValidationResult: + """Validate adapter/workflow compatibility and a public-safety boundary. + + A valid result proves only that the two public contracts are structurally + compatible and contain no high-confidence leak patterns known to this + validator. Manual privacy review and all runtime/media evidence remain + separate requirements. + """ + + adapter_result = validate_local_skill_adapter(adapter) + workflow_result = validate_workflow(workflow, action_library) + errors = [f"adapter: {error}" for error in adapter_result.errors] + errors.extend(f"workflow: {error}" for error in workflow_result.errors) + warnings = [f"adapter: {warning}" for warning in adapter_result.warnings] + warnings.extend(f"workflow: {warning}" for warning in workflow_result.warnings) + + adapter_dict = adapter if isinstance(adapter, dict) else {} + workflow_dict = workflow if isinstance(workflow, dict) else {} + adapter_stages = { + stage.get("id"): stage + for stage in adapter_dict.get("stages", []) + if isinstance(stage, dict) and isinstance(stage.get("id"), str) + } + workflow_stages = { + stage.get("id"): stage + for stage in workflow_dict.get("stages", []) + if isinstance(stage, dict) and isinstance(stage.get("id"), str) + } + + matched_stage_count = 0 + for stage_id, workflow_stage in workflow_stages.items(): + adapter_stage = adapter_stages.get(stage_id) + if adapter_stage is None: + errors.append(f"integration: workflow stage '{stage_id}' is not declared by the adapter.") + continue + matched_stage_count += 1 + comparisons = ( + ("skill", "skill_label", "Skill label"), + ("output", "output_contract", "output contract"), + ("evidence", "evidence", "evidence semantics"), + ) + for workflow_field, adapter_field, description in comparisons: + if workflow_stage.get(workflow_field) != adapter_stage.get(adapter_field): + errors.append( + f"integration: stage '{stage_id}' {description} does not match the adapter." + ) + + required_outputs = {WORKFLOW_CONTRACT, SHOT_PLAN_CONTRACT} + action_requests = workflow_dict.get("action_requests") + if isinstance(action_requests, list) and action_requests: + required_outputs.add(ACTION_REQUEST_CONTRACT) + if workflow_dict.get("prompt_pack") is not None: + required_outputs.add(PROMPT_CONTRACT) + if workflow_dict.get("qc_report") is not None: + required_outputs.add(QC_CONTRACT) + + workflow_outputs = { + stage.get("output") + for stage in workflow_stages.values() + if isinstance(stage.get("output"), str) + } + for output_contract in sorted(required_outputs - workflow_outputs): + errors.append( + f"integration: no workflow stage declares required output '{output_contract}'." + ) + + if isinstance(action_requests, list): + for index, request in enumerate(action_requests): + if not isinstance(request, dict): + continue + stage_id = request.get("stage_id") + stage = workflow_stages.get(stage_id) + if stage is not None and stage.get("output") != ACTION_REQUEST_CONTRACT: + errors.append( + f"integration: action_requests[{index}].stage_id must reference a stage " + f"that outputs '{ACTION_REQUEST_CONTRACT}'." + ) + + safety_errors: list[str] = [] + _audit_public_value(adapter_dict, "adapter", safety_errors) + _audit_public_value(workflow_dict, "workflow", safety_errors) + errors.extend(safety_errors) + + unused_adapter_stage_ids = sorted(set(adapter_stages) - set(workflow_stages)) + summary = { + "adapter_valid": adapter_result.valid, + "workflow_valid": workflow_result.valid, + "adapter_stage_count": len(adapter_stages), + "workflow_stage_count": len(workflow_stages), + "matched_stage_count": matched_stage_count, + "unused_adapter_stage_ids": unused_adapter_stage_ids, + "required_output_contracts": sorted(required_outputs), + "compiled_action_count": workflow_result.summary.get("compiled_action_count", 0), + "public_safety_issue_count": len(safety_errors), + "manual_privacy_review_required": True, + } + return ValidationResult(not errors, errors, warnings, summary) + + +def validate_skill_integration_files( + adapter_path: str | Path, + workflow_path: str | Path, + action_library_path: str | Path | None = None, +) -> ValidationResult: + """Load and cross-validate public adapter and workflow JSON files.""" + + adapter = load_json(adapter_path) + workflow = load_json(workflow_path) + library = load_action_library(action_library_path) if action_library_path else None + return validate_skill_integration(adapter, workflow, library) diff --git a/src/aigc_director_kit/verification.py b/src/aigc_director_kit/verification.py index cc4cc14..ae5a06d 100644 --- a/src/aigc_director_kit/verification.py +++ b/src/aigc_director_kit/verification.py @@ -13,6 +13,7 @@ from .adapter import validate_local_skill_adapter_file from .actions import compile_action_request, list_actions, load_action_library from .contract import ValidationResult, validate_plan_file +from .integration import validate_skill_integration_files from .prompt import validate_prompt_pack_file from .qc import validate_qc_report_file from .runtime import build_runtime_handoff_file @@ -157,6 +158,16 @@ def run_public_example_verification(root: Path | None = None) -> dict[str, Any]: examples / "skill_workflow_case.json", lambda: validate_workflow_file(examples / "skill_workflow_case.json", library_path), ), + _validation_check( + "local-skill-integration", + project_root, + examples / "skill_workflow_case.json", + lambda: validate_skill_integration_files( + examples / "local_skill_adapter_case.json", + examples / "skill_workflow_case.json", + library_path, + ), + ), _validation_check( "prompt-pack", project_root, @@ -257,7 +268,9 @@ def verify_runtime_handoff() -> dict[str, Any]: }, "checks": checks, "boundary": ( - "This verifies public JSON contracts and deterministic dry-run handoffs only. " - "It does not render, generate, open media, or perform final video QC." + "This verifies public JSON contracts, adapter/workflow compatibility, " + "high-confidence leak patterns, and deterministic dry-run handoffs only. " + "Manual privacy review is still required. It does not render, generate, " + "open media, or perform final video QC." ), } diff --git a/tests/test_cli.py b/tests/test_cli.py index 0cb38d5..6f77aeb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -38,6 +38,21 @@ def test_validate_local_skill_adapter_json_mode(self) -> None: 0, ) + def test_validate_skill_integration_json_mode(self) -> None: + self.assertEqual( + main( + [ + "validate-skill-integration", + str(ROOT / "examples" / "local_skill_adapter_case.json"), + str(ROOT / "examples" / "skill_workflow_case.json"), + "--library", + str(ROOT / "examples" / "action_library.json"), + "--json", + ] + ), + 0, + ) + def test_compile_action_refuses_overwrite(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: output = Path(temp_dir) / "action.json" diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..5ea0b46 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,136 @@ +import json +import unittest +from pathlib import Path + +from aigc_director_kit.actions import load_action_library +from aigc_director_kit.integration import ( + validate_skill_integration, + validate_skill_integration_files, +) + + +ROOT = Path(__file__).resolve().parents[1] + + +class SkillIntegrationTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.adapter_path = ROOT / "examples" / "local_skill_adapter_case.json" + cls.workflow_path = ROOT / "examples" / "skill_workflow_case.json" + cls.library_path = ROOT / "examples" / "action_library.json" + cls.library = load_action_library(cls.library_path) + + def load_examples(self) -> tuple[dict, dict]: + adapter = json.loads(self.adapter_path.read_text(encoding="utf-8")) + workflow = json.loads(self.workflow_path.read_text(encoding="utf-8")) + return adapter, workflow + + def test_public_adapter_and_workflow_are_compatible(self) -> None: + result = validate_skill_integration_files( + self.adapter_path, + self.workflow_path, + self.library_path, + ) + + self.assertTrue(result.valid, result.errors) + self.assertEqual(result.summary["matched_stage_count"], 3) + self.assertEqual( + result.summary["unused_adapter_stage_ids"], + ["prompt-contract", "quality-evidence"], + ) + self.assertEqual(result.summary["public_safety_issue_count"], 0) + self.assertTrue(result.summary["manual_privacy_review_required"]) + + def test_integration_without_action_library_keeps_an_explicit_warning(self) -> None: + result = validate_skill_integration_files(self.adapter_path, self.workflow_path) + + self.assertTrue(result.valid, result.errors) + self.assertEqual(result.summary["compiled_action_count"], 0) + self.assertTrue(any("was not compiled" in warning for warning in result.warnings)) + + def test_skill_label_mismatch_is_rejected(self) -> None: + adapter, workflow = self.load_examples() + workflow["stages"][0]["skill"] = "another-director" + + result = validate_skill_integration(adapter, workflow, self.library) + + self.assertFalse(result.valid) + self.assertTrue(any("Skill label" in error for error in result.errors)) + + def test_output_and_evidence_mismatches_are_rejected(self) -> None: + adapter, workflow = self.load_examples() + workflow["stages"][0]["output"] = "custom-shot-plan" + workflow["stages"][1]["evidence"] = "designed" + + result = validate_skill_integration(adapter, workflow, self.library) + + self.assertFalse(result.valid) + self.assertTrue(any("output contract" in error for error in result.errors)) + self.assertTrue(any("evidence semantics" in error for error in result.errors)) + + def test_action_request_must_use_an_action_producer(self) -> None: + adapter, workflow = self.load_examples() + workflow["action_requests"][0]["stage_id"] = "direction" + + result = validate_skill_integration(adapter, workflow, self.library) + + self.assertFalse(result.valid) + self.assertTrue(any("action_requests[0].stage_id" in error for error in result.errors)) + + def test_undeclared_workflow_stage_is_rejected(self) -> None: + adapter, workflow = self.load_examples() + workflow["stages"].append( + { + "id": "private-stage", + "skill": "private-skill", + "role": "Private stage must not bypass the public adapter.", + "output": "private-output", + "evidence": "designed", + "status": "validated", + } + ) + + result = validate_skill_integration(adapter, workflow, self.library) + + self.assertFalse(result.valid) + self.assertTrue(any("not declared by the adapter" in error for error in result.errors)) + + def test_required_workflow_producer_cannot_be_omitted(self) -> None: + adapter, workflow = self.load_examples() + workflow["stages"] = [stage for stage in workflow["stages"] if stage["id"] != "coordination"] + + result = validate_skill_integration(adapter, workflow, self.library) + + self.assertFalse(result.valid) + self.assertTrue( + any("aigc-director-skill-workflow" in error for error in result.errors) + ) + + def test_local_path_is_rejected_without_echoing_its_value(self) -> None: + adapter, workflow = self.load_examples() + workflow["handoff"]["constraints"].append(r"Read Z:\redacted\brief.md") + + result = validate_skill_integration(adapter, workflow, self.library) + + errors = "\n".join(result.errors) + self.assertFalse(result.valid) + self.assertIn("local or network path", errors) + self.assertNotIn(r"Z:\redacted\brief.md", errors) + + def test_credential_field_and_email_are_rejected(self) -> None: + adapter, workflow = self.load_examples() + adapter["stages"][0]["role"] = "Contact editor@example.com before export." + workflow["api_key"] = "not-a-real-key" + + result = validate_skill_integration(adapter, workflow, self.library) + + errors = "\n".join(result.errors) + self.assertFalse(result.valid) + self.assertIn("credential-bearing field", errors) + self.assertIn("email address", errors) + self.assertNotIn("not-a-real-key", errors) + self.assertNotIn("editor@example.com", errors) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_verification.py b/tests/test_verification.py index 28da719..9aee33f 100644 --- a/tests/test_verification.py +++ b/tests/test_verification.py @@ -16,7 +16,7 @@ def test_verifies_all_public_examples_without_local_paths(self) -> None: self.assertTrue(report["valid"]) self.assertEqual(report["version"], __version__) - self.assertEqual(report["summary"], {"check_count": 12, "passed_check_count": 12}) + self.assertEqual(report["summary"], {"check_count": 13, "passed_check_count": 13}) self.assertNotIn(str(ROOT), json.dumps(report, ensure_ascii=False)) self.assertTrue(all(check["valid"] for check in report["checks"]))