diff --git a/docs/CSL_ONBOARDING.md b/docs/CSL_ONBOARDING.md new file mode 100644 index 00000000..9dfa32d3 --- /dev/null +++ b/docs/CSL_ONBOARDING.md @@ -0,0 +1,136 @@ +# CSL 1.0 onboarding with darnit + +How to make any specification repository compliant with the +[Community Specification License 1.0](https://github.com/CommunitySpecification/1.0) +using the `community-spec` framework (`darnit-csl`), and reproduce the process +we used for DSSE, in-toto, TUF, and Uptane. + +## What it produces + +The required CSL file set, generated from templates and audited: + +| File | Content | +|---|---| +| `governance/00-contributor-license-agreement.md` | CSL Contributor License Agreement 1.0 (verbatim, with working links to the upstream documents) | +| `governance/01-community-specification-license-v1.md` | Community Specification License 1.0 (verbatim) | +| `governance/02-scope.md` | The Working Group's Scope — **you draft this; it bounds the patent commitment** | +| `governance/03-notices.md` | Notices with the Code of Conduct pointer (org-first, see below) | +| `governance/04-license.md` | Dual license: CSL for the spec, your chosen license for source/sample code | +| `governance/05-governance.md` | Full CSL Governance Policy, or a stub referencing umbrella governance (e.g. CNCF) | +| `README` links | A "Governance & Licensing" section pointing at the files above | + +Seven controls verify it: `CSL-01.01` (CLA), `CSL-01.02` (license, detected by +content under any filename), `CSL-02.01` (scope is real, not the bracketed +placeholder), `CSL-03.01` (notices name a real contact **or** point at an +org-level Code of Conduct — blanks, placeholder names, and leftover drafting +guidance all FAIL), `CSL-04.01` (dual license), `CSL-05.01` (CSL-specific +governance), `CSL-06.01` (README discoverability). + +## What you must supply + +1. **Scope prose.** Write what the Working Group standardizes and what it + excludes, grounded in the repository's actual documents — never generic + filler, never the bracketed template text. This is a legal boundary, so a + human must review it before any PR. +2. **Code of Conduct — org-first.** If the project already has a Code of + Conduct (its own `CODE_OF_CONDUCT` file, the org's community or `.github` + repo, or a foundation CoC such as CNCF, LF, or JDF), point at it: + `coc_policy='org'` plus `coc_reference` — one markdown sentence linking + that document. The notices then name no individuals; the linked CoC + defines the reporting procedure. The remediate tool refuses individual + contacts when the repo itself already ships a CoC file. Only when no such + CoC exists, supply two named individuals with an email or handle each — + never a mailing list. +3. **Source-code license** for sample/reference code (e.g. `Apache-2.0`, `MIT`). +4. **Governance mode**: `csl` (ship the full CSL policy) or `umbrella` + (reference existing governance, e.g. a CNCF project charter). + +## Path 1 — one-command driver (recommended) + +From the darnit repo root: + +```bash +# 1. Baseline audit; also writes a scope template for you to fill in +uv run python scripts/csl_onboard.py --repo / --audit-only + +# 2a. Project already has a Code of Conduct (org-first): +uv run python scripts/csl_onboard.py --repo / \ + --scope-file scopes/.md \ + --coc-policy org \ + --coc-reference "X is a [CNCF](https://www.cncf.io/) project and follows the [X Code of Conduct]()." \ + --code-license Apache-2.0 --spec-name "" + +# 2b. No existing CoC anywhere — named-individuals fallback: +uv run python scripts/csl_onboard.py --repo / \ + --scope-file scopes/.md \ + --contacts "Name One , Name Two " \ + --code-license Apache-2.0 --spec-name "" +``` + +The driver clones, branches, audits, remediates from your scope file, scans +every generated file for placeholder text, re-audits, and prints the fork/PR +commands. Nothing is written without `--scope-file`. + +## Path 2 — agentic, via MCP (`darnit serve`) + +Register the server with your MCP client (Claude Code, etc.): + +```json +{ "mcpServers": { "darnit-csl": { + "command": "uv", + "args": ["run", "darnit", "serve", "--framework", "community-spec"] } } } +``` + +The client gets three tools: `audit_community_spec`, +`list_community_spec_controls`, and `remediate_community_spec`. The intended +loop: audit → the agent drafts the Scope **from the repository's own +specification documents** and **checks for an existing Code of Conduct** +(repo file, org community/.github repo, foundation CoC) → you review → the +agent calls `remediate_community_spec(local_path, scope=..., coc_policy=..., +coc_reference=... / coc_contacts=...)` → inline re-audit. The tool enforces +org-first itself: if the repo carries its own CoC file, individual contacts +are rejected with a pointer to org mode. Under `serve`, content-quality +checks run through the LLM first; under plain `darnit audit` they fall back +to deterministic checks. + +## Path 3 — local checkout, minimal + +`scripts/csl_manual_remediate.py` runs remediation + re-audit against a repo +you have already cloned: edit its `ANSWERS` dict (scope, contacts or +`csl_coc_policy='org'` + `csl_coc_reference`, licenses, mode — this edit +stays local, don't commit it), then: + +```bash +uv run python scripts/csl_manual_remediate.py +``` + +## Reviewing and opening the PR + +- `git status` in the target repo must show **only** `governance/*` changes + (plus README links on first run). Never commit `.project/` — that is + darnit's per-repo state. +- Read `02-scope.md` and `03-notices.md` yourself; the audit checks form, + humans check meaning. +- Commit, push to your fork, open a **draft** PR, and note in the description + that the spec is CSL-licensed and code is under the chosen license. + +## Re-running after review + +`02-scope.md` and `03-notices.md` are regenerated on every run +(`overwrite = true`). Verbatim legal files (`00`, `01`, `04`, `05`) are +protected (`overwrite = false`): to regenerate one — e.g. after a template +fix — delete the file first, then re-run. + +## Known upstream quirk + +The upstream CSL repo renamed its files to the `NN-name.md` convention but +never updated its own CLA boilerplate, which still cites old `N._Name.md` +names. The `csl_cla` template links to the files that actually exist; if a +reviewer flags "broken filenames," point them upstream. + +## Real examples of the org-first rule + +- **in-toto** (CNCF): points at the [in-toto Community Code of Conduct](https://github.com/in-toto/community/blob/main/CODE-OF-CONDUCT.md), which abides by the CNCF CoC. +- **TUF** (CNCF): points at the [TUF Community Code of Conduct](https://github.com/theupdateframework/community/blob/main/CODE-OF-CONDUCT.md). +- **Uptane** (LF/JDF): points at the [JDF Code of Conduct](https://jointdevelopment.org/policies/code-of-conduct/). +- **DSSE** (no org-level CoC): named individuals — the fallback case. diff --git a/packages/darnit-csl/README.md b/packages/darnit-csl/README.md new file mode 100644 index 00000000..338c0a26 --- /dev/null +++ b/packages/darnit-csl/README.md @@ -0,0 +1,68 @@ +# darnit-csl + +Community Specification License (CSL 1.0) compliance for +[darnit](https://github.com/kusari-oss/darnit). + +It audits — and remediates — the file set the Linux Foundation / Joint +Development Foundation asks for in a repository that develops a specification +under the **Community Specification License 1.0**: + +| File (in `governance/`, `license/`, or repo root) | Control | What it is | +|---|---|---| +| `00-contributor-license-agreement.md` | `CSL-01.01` | Contributor License Agreement (static) | +| `01-community-specification-license-v1.md` | `CSL-01.02` | CSL 1.0 license text (static) | +| `02-scope.md` | `CSL-02.01` | Working Group **Scope** — per-project; can be LLM-drafted | +| `03-notices.md` | `CSL-03.01` | **Notices** + Code of Conduct contact(s) | +| `04-license.md` | `CSL-04.01` | Dual license: CSL for the spec, MIT/Apache for code | +| `05-governance.md` | `CSL-05.01` | Governance — full CSL policy **or** a reference to an umbrella project's governance | +| README links | `CSL-06.01` | Scope and Notices are discoverable from the README | + +## How each control works (the 7-step pattern) + +1. Look in `.project/` for confirmed context. +2. Does the file exist (`governance/`, `license/`, or repo root)? +3. Is the content valid — i.e. no leftover template placeholder? If yes, **PASS**, done. +4. If not, collect data and ask the user (`requires_context` prompts). +5. Persist the answers to `.project/`. +6. Generate the file from a template (`file_create`). +7. Refine the generated file from project context via the LLM (`llm_enhance`). + +The two content-validated files (`02-scope.md`, `03-notices.md`) only pass when +the template placeholder is gone — a deterministic check that runs in +`darnit audit` without an LLM, with an `llm_eval` nuance pass for the MCP server. + +## Governance & Code of Conduct: reference vs. subsume + +Projects that already follow an umbrella governance / CoC (e.g. CNCF) do **not** +need to adopt the CSL versions wholesale: + +- `csl_governance_mode = "umbrella"` generates a short `05-governance.md` that + **references** the existing governance (it is not superseded) and documents the + Community Specification roles and decision-making on top of it. +- `csl_coc_policy = "umbrella"` makes `03-notices.md` cite an existing Code of + Conduct (e.g. the CNCF CoC) instead of shipping the CSL one. + +The defaults adopt the full CSL governance and ship CSL contacts; the umbrella +modes are opt-in. + +## Try it + +```bash +# Audit a spec repo against CSL 1.0 +uv run darnit audit --framework packages/darnit-csl/src/darnit_csl/community-spec.toml /path/to/spec-repo + +# Audit just the required-files profile +uv run darnit audit --framework packages/darnit-csl/src/darnit_csl/community-spec.toml --profile required_files /path/to/spec-repo + +# Show the execution plan +uv run darnit plan --framework packages/darnit-csl/src/darnit_csl/community-spec.toml /path/to/spec-repo +``` + +Once installed (`pip install darnit-csl`), it is discovered by name: +`darnit audit --framework community-spec /path/to/spec-repo`. + +## License + +Apache-2.0. The bundled CSL templates are reproduced from the +[Community Specification 1.0](https://github.com/CommunitySpecification/1.0) +repository. diff --git a/packages/darnit-csl/pyproject.toml b/packages/darnit-csl/pyproject.toml new file mode 100644 index 00000000..abded910 --- /dev/null +++ b/packages/darnit-csl/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "darnit-csl" +version = "0.1.0" +description = "Community Specification License (CSL 1.0) compliance implementation for darnit" +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +authors = [ + { name = "Kusari", email = "info@kusari.dev" }, +] +keywords = ["compliance", "darnit", "community-specification", "csl", "plugin"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Documentation", +] +dependencies = [ + "darnit-core>=0.1.0", +] + +[project.urls] +Homepage = "https://github.com/kusari-oss/darnit" +Repository = "https://github.com/kusari-oss/darnit" +Issues = "https://github.com/kusari-oss/darnit/issues" + +# Discoverability: the implementation entry point. +[project.entry-points."darnit.implementations"] +community-spec = "darnit_csl:register" + +# Framework TOML discovery (locate the config without importing the package). +[project.entry-points."darnit.frameworks"] +community-spec = "darnit_csl:get_framework_path" +community-spec-optional = "darnit_csl:get_optional_framework_path" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/darnit_csl"] + +# community-spec.toml and templates/ live inside src/darnit_csl/, so hatchling +# includes them in the wheel automatically. diff --git a/packages/darnit-csl/src/darnit_csl/__init__.py b/packages/darnit-csl/src/darnit_csl/__init__.py new file mode 100644 index 00000000..06f4cd48 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/__init__.py @@ -0,0 +1,68 @@ +"""darnit-csl — Community Specification License (CSL 1.0) compliance plugin. + +Adds a darnit compliance standard for repositories that develop specifications +under the Community Specification License 1.0. It audits for, and remediates, +the file set the Linux Foundation / Joint Development Foundation requires +(CLA, license, scope, notices, dual-license, governance). +""" + +from pathlib import Path + +from .implementation import CommunitySpecImplementation + + +def register() -> CommunitySpecImplementation: + """Entry point called by darnit plugin discovery. + + Wired in pyproject.toml via: + + [project.entry-points."darnit.implementations"] + community-spec = "darnit_csl:register" + """ + return CommunitySpecImplementation() + + +def get_framework_path() -> Path: + """Entry point for framework TOML config discovery. + + Wired in pyproject.toml via: + + [project.entry-points."darnit.frameworks"] + community-spec = "darnit_csl:get_framework_path" + """ + return CommunitySpecImplementation().get_framework_config_path() + + +def get_optional_framework_path() -> Path: + """Entry point for the OPTIONAL (facultative) framework TOML. + + Registers the separate `community-spec-optional` framework, which presence- + checks the optional Community Specification files (06 contributing, 07 spec- + template, 08 code-of-conduct). Kept separate so it never affects the required + CSL compliance score. Wired in pyproject.toml via: + + [project.entry-points."darnit.frameworks"] + community-spec-optional = "darnit_csl:get_optional_framework_path" + """ + from importlib.resources import files + + resource = files(__package__) / "community-spec-optional.toml" + path = Path(str(resource)) + if not path.is_file(): + raise FileNotFoundError( + f"community-spec-optional.toml not found in installed darnit_csl " + f"package at {path}. This indicates a broken build; check the " + f"wheel's force-include configuration." + ) + return path + + +__all__ = [ + "CommunitySpecImplementation", + "register", + "get_framework_path", + "get_optional_framework_path", +] + +# Register custom sieve handlers on import. +from . import handlers # noqa: E402,F401 diff --git a/packages/darnit-csl/src/darnit_csl/community-spec-optional.toml b/packages/darnit-csl/src/darnit_csl/community-spec-optional.toml new file mode 100644 index 00000000..9aac98e3 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/community-spec-optional.toml @@ -0,0 +1,98 @@ +# ============================================================================= +# darnit-csl - Community Specification, OPTIONAL files (facultative) +# ============================================================================= +# +# The Community Specification repo (github.com/CommunitySpecification/1.0) ships +# nine numbered files, 00 through 08. The required set per the Linux Foundation +# is 00 to 05 and lives in the main `community-spec` framework. The remaining +# three are optional ("may not be necessary" per LF), so they are kept in this +# SEPARATE framework to avoid affecting the required-compliance score: +# +# 06-contributing.md contribution guide +# 07-spec-template.md specification document template +# 08-code-of-conduct.md Code of Conduct text +# +# Facultative: this is a presence check only (no remediation). Run it explicitly +# and separately from the required audit: +# +# darnit audit --framework community-spec-optional +# +# A FAIL here is informational, not a compliance gap. + +# ----------------------------------------------------------------------------- +[metadata] +name = "community-spec-optional" +display_name = "Community Specification - optional files (06/07/08)" +version = "0.1.0" +schema_version = "0.1.0-alpha" +spec_version = "CSL 1.0" +description = "Facultative presence check for the optional Community Specification files (contributing, spec-template, code-of-conduct)" +url = "https://github.com/CommunitySpecification/1.0" + +[defaults] +check_adapter = "builtin" +remediation_adapter = "builtin" + +# ----------------------------------------------------------------------------- +# CSL-OPT-01 -> 06-contributing.md +# ----------------------------------------------------------------------------- +[controls."CSL-OPT-01"] +name = "ContributionGuide" +description = "Optional: a contribution guide is present (Community Specification file 06-contributing.md, or a repo CONTRIBUTING.md)." +tags = { level = 1, domain = "CSL", optional = true } +docs_url = "https://github.com/CommunitySpecification/1.0/blob/main/06-contributing.md" +location_hint = "06-contributing.md or CONTRIBUTING.md" + +[[controls."CSL-OPT-01".passes]] +handler = "file_exists" +files = [ + "governance/06-contributing.md", + "license/06-contributing.md", + "06-contributing.md", + "CONTRIBUTING.md", + "docs/CONTRIBUTING.md", + ".github/CONTRIBUTING.md", +] + +# ----------------------------------------------------------------------------- +# CSL-OPT-02 -> 07-spec-template.md +# ----------------------------------------------------------------------------- +[controls."CSL-OPT-02"] +name = "SpecificationTemplate" +description = "Optional: a specification document template is present (Community Specification file 07-spec-template.md)." +tags = { level = 1, domain = "CSL", optional = true } +docs_url = "https://github.com/CommunitySpecification/1.0/blob/main/07-spec-template.md" +location_hint = "07-spec-template.md" + +[[controls."CSL-OPT-02".passes]] +handler = "file_exists" +files = [ + "governance/07-spec-template.md", + "license/07-spec-template.md", + "07-spec-template.md", + "docs/spec-template.md", + "templates/spec-template.md", + "SPEC-TEMPLATE.md", +] + +# ----------------------------------------------------------------------------- +# CSL-OPT-03 -> 08-code-of-conduct.md +# ----------------------------------------------------------------------------- +[controls."CSL-OPT-03"] +name = "CodeOfConductFile" +description = "Optional: a Code of Conduct file is present (Community Specification file 08-code-of-conduct.md, or a repo CODE_OF_CONDUCT.md)." +tags = { level = 1, domain = "CSL", optional = true } +docs_url = "https://github.com/CommunitySpecification/1.0/blob/main/08-code-of-conduct.md" +location_hint = "08-code-of-conduct.md or CODE_OF_CONDUCT.md" + +[[controls."CSL-OPT-03".passes]] +handler = "file_exists" +files = [ + "governance/08-code-of-conduct.md", + "license/08-code-of-conduct.md", + "08-code-of-conduct.md", + "CODE_OF_CONDUCT.md", + "CODE-OF-CONDUCT.md", + "docs/CODE_OF_CONDUCT.md", + ".github/CODE_OF_CONDUCT.md", +] diff --git a/packages/darnit-csl/src/darnit_csl/community-spec.toml b/packages/darnit-csl/src/darnit_csl/community-spec.toml new file mode 100644 index 00000000..7d840d5d --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/community-spec.toml @@ -0,0 +1,637 @@ +# ============================================================================= +# darnit-csl - Community Specification License (CSL 1.0) compliance +# ============================================================================= +# +# Implements the file set the Linux Foundation / Joint Development Foundation +# requires for a repository that develops a specification under the +# Community Specification License 1.0: +# +# governance/00-contributor-license-agreement.md (static) +# governance/01-community-specification-license-v1.md (static) +# governance/02-scope.md (per-project - the "art"; LLM-drafted) +# governance/03-notices.md (Code of Conduct contacts) +# governance/04-license.md (dual: CSL for spec, MIT/Apache for code) +# governance/05-governance.md (full CSL policy OR reference an umbrella +# project's governance, e.g. CNCF) +# +# Each control follows the same 7 steps: +# 1. Look in .project/ for confirmed context +# 2. Does the file exist (governance/, license/, or repo root)? +# 3. Is the content valid (no leftover template placeholders)? -> PASS, done +# 4. If not, collect data and ask the user (requires_context prompts) +# 5. Persist answers to .project/ (project_update + context store_as) +# 6. Generate the file from a template (file_create) +# 7. Refine the generated file from project context via the LLM (llm_enhance) +# +# The directory the LF asks for can be /governance OR /license; this plugin +# treats `governance/` as canonical for generation and accepts either (plus the +# repo root) when auditing. + +# ----------------------------------------------------------------------------- +[metadata] +name = "community-spec" +display_name = "Community Specification License (CSL 1.0)" +version = "0.1.0" +schema_version = "0.1.0-alpha" +spec_version = "CSL 1.0" +description = "Compliance for repositories that develop specifications under the Community Specification License 1.0" +url = "https://github.com/CommunitySpecification/1.0" + +[defaults] +check_adapter = "builtin" +remediation_adapter = "builtin" + +# ----------------------------------------------------------------------------- +# MCP integration. Built-in audit + list tools are enough for a TOML-only +# plugin; remediation is driven generically from the [remediation] blocks below. +# ----------------------------------------------------------------------------- +[mcp] +name = "community-spec" +description = "Audit and remediate Community Specification License (CSL 1.0) compliance." + +[mcp.tools.audit_community_spec] +builtin = "audit" +description = "Run the CSL 1.0 compliance audit on a specification repository." + +[mcp.tools.list_community_spec_controls] +builtin = "list_controls" +description = "List the CSL 1.0 compliance controls." + +# Write phase: renders every CSL file from templates using client-supplied +# context (the client drafts the Scope first). Module-path handler, not a +# builtin, so it lives in the plugin. +[mcp.tools.remediate_community_spec] +handler = "darnit_csl.mcp_tools:remediate_community_spec" +description = "Write the CSL 1.0 file set (governance/00..05) into a spec repo from a drafted scope and context, then re-audit. Code of Conduct is org-first: BEFORE calling, check whether the project already has one (a repo CODE_OF_CONDUCT file, the org's community or .github repo, or a foundation CoC such as CNCF, LF, or JDF). If it does, pass coc_policy='org' with coc_reference = one markdown sentence linking it; the notices will point there and name no individuals. Only when no such CoC exists, pass two named individuals in coc_contacts (never a mailing list)." + +# ----------------------------------------------------------------------------- +# Audit profiles +# ----------------------------------------------------------------------------- +[audit_profiles.required_files] +description = "The files the LF requires in the spec repo (CLA, license, scope, notices, licenses, governance)." +controls = [ + "CSL-01.01", "CSL-01.02", "CSL-02.01", + "CSL-03.01", "CSL-04.01", "CSL-05.01", +] + +[audit_profiles.discoverability] +description = "Scope and notices must be easy to discover from the README." +controls = ["CSL-06.01"] + +# ----------------------------------------------------------------------------- +# Templates +# ----------------------------------------------------------------------------- +[templates.csl_cla] +description = "Community Specification Contributor License Agreement 1.0 (verbatim)." +file = "templates/csl_cla.tmpl" + +[templates.csl_license] +description = "Community Specification License 1.0 (verbatim)." +file = "templates/csl_license.tmpl" + +[templates.csl_scope] +description = "Scope statement - filled from context or left as a detectable placeholder for LLM drafting." +file = "templates/csl_scope.tmpl" + +[templates.csl_notices] +description = "Notices file with Code of Conduct contact(s) and license-acceptance sections." +file = "templates/csl_notices.tmpl" + +[templates.csl_license_dual] +description = "Dual license file: CSL for the spec, selectable code license for source." +file = "templates/csl_license_dual.tmpl" + +[templates.csl_governance] +description = "Full Community Specification Governance Policy 1.0 (verbatim)." +file = "templates/csl_governance.tmpl" + +[templates.csl_governance_reference] +description = "Governance stub that references an umbrella project's governance (e.g. CNCF) instead of subsuming it." +file = "templates/csl_governance_reference.tmpl" + +# ============================================================================= +# Context keys (the questions asked in step 4; persisted in step 5) +# ============================================================================= + +[context.csl_spec_name] +type = "string" +prompt = "What is the human-readable name of the specification / Working Group?" +hint = "Used in generated documents. Defaults to the repository name." +examples = ["The Update Framework (TUF)", "in-toto Attestation"] +affects = ["CSL-02.01", "CSL-03.01"] +store_as = "legal.csl.spec_name" +required = false + +[context.csl_working_group_scope] +type = "string" +prompt = "Describe the Scope of this Working Group (what the specification covers)." +hint = "This sets the bounds of each contributor's and licensee's patent commitment. You may provide prose here, or leave it blank and let the agent draft a Scope from the repository, which you then review." +no_detect_hint = "No existing Scope statement was found. Provide a description, or let the agent draft one for your review." +examples = ["This Working Group standardizes the metadata format and verification workflow for ..."] +affects = ["CSL-02.01"] +store_as = "legal.csl.scope_text" +required = false + +[context.csl_coc_contacts] +type = "string" +prompt = "Who receives Code of Conduct complaints for this Working Group? List two named individuals (not a mailing list)." +hint = "Names plus a contact handle/email each. Two people are recommended so a complaint about one can go to the other." +examples = ["Jane Doe (jane@example.org), John Roe (@johnroe)"] +affects = ["CSL-03.01"] +store_as = "legal.csl.coc_contacts" +required = false + +[context.csl_coc_policy] +type = "enum" +values = ["csl", "umbrella"] +prompt = "For the Code of Conduct, ship the Community Specification CoC, or reference an umbrella project's CoC (e.g. CNCF)?" +hint = "'csl' = use the CoC that ships with Community Specification. 'umbrella' = cite an existing CoC such as the CNCF Code of Conduct." +affects = ["CSL-03.01"] +store_as = "legal.csl.coc_policy" +required = false + +[context.csl_coc_reference] +type = "string" +prompt = "If referencing an umbrella Code of Conduct, name and link it." +hint = "Only needed when the CoC policy is 'umbrella'." +examples = ["CNCF Code of Conduct (https://github.com/cncf/foundation/blob/main/code-of-conduct.md)"] +affects = ["CSL-03.01"] +store_as = "legal.csl.coc_reference" +required = false + +[context.csl_governance_mode] +type = "enum" +values = ["csl", "umbrella"] +prompt = "For governance, adopt the full Community Specification Governance Policy, or reference an existing umbrella governance (e.g. CNCF) and keep only the CSL roles?" +hint = "'csl' = ship the full Community Specification Governance Policy 1.0. 'umbrella' = reference the project's existing governance (it is not superseded) and document the CSL roles/decision-making on top." +affects = ["CSL-05.01"] +store_as = "legal.csl.governance_mode" +required = false + +[context.csl_governance_reference] +type = "string" +prompt = "If referencing an umbrella governance, name and link it." +hint = "Only needed when the governance mode is 'umbrella'." +examples = ["CNCF project governance (https://github.com/cncf/foundation/blob/main/charter.md)"] +affects = ["CSL-05.01"] +store_as = "legal.csl.governance_reference" +required = false + +[context.csl_code_license] +type = "string" +prompt = "Which license applies to source/sample code in the spec repo?" +hint = "Any SPDX identifier. The Community Specification default is MIT; projects also use Apache-2.0, BSD-3-Clause, or copyleft licenses such as AGPL-3.0 (e.g. TAF)." +examples = ["MIT", "Apache-2.0", "AGPL-3.0", "BSD-3-Clause"] +affects = ["CSL-04.01"] +store_as = "legal.csl.code_license" +required = false + +# ============================================================================= +# Controls +# ============================================================================= + +# ----------------------------------------------------------------------------- +# CSL-01.01 - Contributor License Agreement (static) +# ----------------------------------------------------------------------------- +[controls."CSL-01.01"] +name = "ContributorLicenseAgreement" +description = "The spec repo contains the Community Specification Contributor License Agreement." +tags = { level = 1, domain = "CSL", legal = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/00-contributor-license-agreement.md" +help_md = """Add the Community Specification CLA to the repository where the +specification is developed. + +**Remediation:** create `governance/00-contributor-license-agreement.md`.""" + +# Content/title detection FIRST: catch the CLA even if stored under a different +# filename. Match the CSL CLA heading in any likely location. +[[controls."CSL-01.01".passes]] +handler = "regex" +files = [ + "CLA.md", "CLA", + "governance/*.md", "license/*.md", + "governance/00-contributor-license-agreement.md", + "license/00-contributor-license-agreement.md", + "00-contributor-license-agreement.md", +] + +[controls."CSL-01.01".passes.pattern.patterns] +csl_cla_title = '(?im)^#+\s*Community Specification Contributor License Agreement\b' + +# Name-based fallback: conclusive FAIL when no CLA file exists at all. +[[controls."CSL-01.01".passes]] +handler = "file_exists" +files = [ + "governance/00-contributor-license-agreement.md", + "license/00-contributor-license-agreement.md", + "00-contributor-license-agreement.md", +] + +[[controls."CSL-01.01".passes]] +handler = "manual" +steps = [ + "Confirm the Community Specification CLA is present in the spec repo.", + "It should sit alongside the license files in /governance or /license.", +] + +[controls."CSL-01.01".remediation] +safe = true +dry_run_supported = true + +[[controls."CSL-01.01".remediation.handlers]] +handler = "file_create" +path = "governance/00-contributor-license-agreement.md" +template = "csl_cla" +overwrite = false + +[controls."CSL-01.01".remediation.project_update] +set = { "legal.csl.cla.path" = "governance/00-contributor-license-agreement.md" } + +# ----------------------------------------------------------------------------- +# CSL-01.02 - Community Specification License file (static) +# ----------------------------------------------------------------------------- +[controls."CSL-01.02"] +name = "SpecificationLicense" +description = "The spec repo contains the Community Specification License 1.0 text." +tags = { level = 1, domain = "CSL", legal = true, license = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/01-community-specification-license-v1.md" +help_md = """Add the Community Specification License 1.0 text to the spec repo. + +**Remediation:** create `governance/01-community-specification-license-v1.md`.""" + +# Content/title detection FIRST: the CSL license may be stored under any +# filename (e.g. TUF ships it as LICENSE.md). Match the CSL heading in any +# likely location. PASS if found, FAIL if a license-ish file exists but is not +# CSL, INCONCLUSIVE if no candidate files (falls through to the name check). +[[controls."CSL-01.02".passes]] +handler = "regex" +files = [ + "LICENSE.md", "LICENSE", "LICENSE.txt", "LICENSE-CSL.md", "COPYING", "COPYING.md", + "governance/*.md", "license/*.md", + "governance/01-community-specification-license-v1.md", + "license/01-community-specification-license-v1.md", + "01-community-specification-license-v1.md", +] + +[controls."CSL-01.02".passes.pattern.patterns] +csl_license_title = '(?im)^#+\s*Community Specification License\b' + +# Name-based fallback: conclusive FAIL when no license file exists at all. +[[controls."CSL-01.02".passes]] +handler = "file_exists" +files = [ + "governance/01-community-specification-license-v1.md", + "license/01-community-specification-license-v1.md", + "01-community-specification-license-v1.md", + "governance/01-community-specification-license-v1.0.md", +] + +[[controls."CSL-01.02".passes]] +handler = "manual" +steps = ["Confirm the Community Specification License 1.0 text is present in the spec repo."] + +[controls."CSL-01.02".remediation] +safe = true +dry_run_supported = true + +[[controls."CSL-01.02".remediation.handlers]] +handler = "file_create" +path = "governance/01-community-specification-license-v1.md" +template = "csl_license" +overwrite = false + +[controls."CSL-01.02".remediation.project_update] +set = { "legal.csl.license.path" = "governance/01-community-specification-license-v1.md" } + +# ----------------------------------------------------------------------------- +# CSL-02.01 - Working Group Scope (content-validated; LLM-drafted) +# ----------------------------------------------------------------------------- +[controls."CSL-02.01"] +name = "WorkingGroupScope" +description = "The repo has a Scope statement, filled in (not the template placeholder)." +tags = { level = 1, domain = "CSL", legal = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/02-scope.md" +help_md = """Provide a Scope statement. The Scope establishes the bounds of each +contributor's and licensee's patent commitment, so a placeholder is not enough. + +**Remediation:** create `governance/02-scope.md` and describe the Working +Group's scope (the agent can draft this from the repository for your review).""" + +# Step 3 (content): under `darnit serve` the LLM judges the scope FIRST, so it +# catches weak or off-topic scopes that regex cannot. Under `darnit audit` +# (no LLM) the sieve stops here and reports PENDING_LLM; the deterministic +# regex/existence passes below still answer non-LLM callers. +[[controls."CSL-02.01".passes]] +handler = "csl_llm_if_present" +prompt = """Evaluate whether the Scope statement actually describes the bounds of +this specification's work (what it standardizes and what it excludes). FAIL if it +contains only the bracketed '[Include a detailed description ...]' placeholder or +is too vague to bound a patent commitment. Otherwise PASS.""" +files_to_include = ["governance/02-scope.md", "license/02-scope.md", "02-scope.md"] +confidence_threshold = 0.7 + +# Deterministic fallback: PASS only if the file exists AND no longer contains the +# bracketed template placeholder. (Missing file -> INCONCLUSIVE -> next pass.) +[[controls."CSL-02.01".passes]] +handler = "regex" +files = ["governance/02-scope.md", "license/02-scope.md", "02-scope.md"] + +[controls."CSL-02.01".passes.pattern.patterns] +scope_filled = '(?s)\A(?!.*\[Include a detailed description).+\S' + +# Existence. Reached only when the file is missing -> conclusive FAIL. +[[controls."CSL-02.01".passes]] +handler = "file_exists" +files = ["governance/02-scope.md", "license/02-scope.md", "02-scope.md"] + +[[controls."CSL-02.01".passes]] +handler = "manual" +steps = ["Confirm governance/02-scope.md describes the Working Group's scope and is not a placeholder."] + +[controls."CSL-02.01".remediation] +safe = true +dry_run_supported = true + +# Only runs when the control FAILS (missing or placeholder), so overwrite is safe. +[[controls."CSL-02.01".remediation.handlers]] +handler = "file_create" +path = "governance/02-scope.md" +template = "csl_scope" +overwrite = true +llm_enhance = "Draft a precise Scope statement for governance/02-scope.md. Read the README and specification sources to describe exactly what this Working Group standardizes and the boundary of the work - this sets the bounds of the contributors' and licensees' patent commitment. Replace the bracketed placeholder entirely. Keep the final line 'Any changes of Scope are not retroactive.'" + +# Step 4: ask for scope (optional - the agent may draft it instead). +[[controls."CSL-02.01".remediation.requires_context]] +key = "csl_working_group_scope" +required = false +warning = "A Scope statement defines the patent commitment. Provide one, or let the agent draft it from the repository for your review." + +[controls."CSL-02.01".remediation.project_update] +set = { "legal.csl.scope.path" = "governance/02-scope.md" } + +# ----------------------------------------------------------------------------- +# CSL-03.01 - Notices + Code of Conduct contact (content-validated) +# ----------------------------------------------------------------------------- +[controls."CSL-03.01"] +name = "NoticesAndCodeOfConductContact" +description = "The repo has a Notices file with a real Code of Conduct contact or an org-level Code of Conduct reference (no blank line, no leftover drafting guidance)." +tags = { level = 1, domain = "CSL", legal = true, governance = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/03-notices.md" +help_md = """Provide the Notices file and fill in who receives Code of Conduct +complaints (org-first: point at an existing project/org Code of Conduct; otherwise two named individuals). + +**Remediation:** create `governance/03-notices.md` and set the CoC contact(s).""" + +# Step 3 (content): under `darnit serve` the LLM judges the CoC contact FIRST. +# Under `darnit audit` (no LLM) the sieve stops here (PENDING_LLM); the +# deterministic passes below still answer non-LLM callers. +[[controls."CSL-03.01".passes]] +handler = "csl_llm_if_present" +prompt = """Evaluate the Notices file's Code of Conduct section. PASS if it either +names a real, reachable contact (a person or role plus an email or handle) OR +points to an established project/org-level Code of Conduct (the repo's own CoC, +an org community CoC, or a foundation CoC such as CNCF / LF / JDF) via a working +reference. FAIL if the contact line is still a blank ('_________________'), the +bracketed drafting guidance ('[Ideally list two different individuals ...]') is +still present, or neither a contact nor such a reference is given.""" +files_to_include = ["governance/03-notices.md", "license/03-notices.md", "03-notices.md"] +confidence_threshold = 0.7 + +# Deterministic fallback: PASS only if the file exists AND the CoC contact line +# is filled (no 5+ underscore blank). Missing -> INCONCLUSIVE -> next pass. +[[controls."CSL-03.01".passes]] +handler = "regex" +files = ["governance/03-notices.md", "license/03-notices.md", "03-notices.md"] + +[controls."CSL-03.01".passes.pattern.patterns] +coc_contact_filled = '(?s)\A(?!.*_{5,})(?!.*\[Ideally list two different individuals)(?!.*(?i:real name|real@email|example\.(com|org|net)|\bTODO\b|\bFIXME\b|\bTBD\b|||your name|firstname|lastname|insert name|name one|name two|john doe|jane doe)).+\S' + +# Existence -> conclusive FAIL when missing. +[[controls."CSL-03.01".passes]] +handler = "file_exists" +files = ["governance/03-notices.md", "license/03-notices.md", "03-notices.md"] + +[[controls."CSL-03.01".passes]] +handler = "manual" +steps = ["Confirm governance/03-notices.md names a Code of Conduct contact (two individuals recommended)."] + +[controls."CSL-03.01".remediation] +safe = true +dry_run_supported = true + +# Step 4: the CoC contact genuinely needs human input -> required = true (blocks +# until confirmed). coc_policy / coc_reference are optional refinements. +[[controls."CSL-03.01".remediation.requires_context]] +key = "csl_coc_contacts" +required = false +warning = "Org-first: if the project already has a Code of Conduct (repo file, org community repo, or a foundation CoC such as CNCF/LF/JDF), set csl_coc_policy='org' with csl_coc_reference and leave contacts empty. Otherwise two named individuals are recommended (never a generic mailing list)." + +[[controls."CSL-03.01".remediation.requires_context]] +key = "csl_coc_policy" +required = false + +[[controls."CSL-03.01".remediation.requires_context]] +key = "csl_coc_reference" +required = false + +[[controls."CSL-03.01".remediation.handlers]] +handler = "file_create" +path = "governance/03-notices.md" +template = "csl_notices" +overwrite = true + +[controls."CSL-03.01".remediation.project_update] +set = { "legal.csl.notices.path" = "governance/03-notices.md" } + +# ----------------------------------------------------------------------------- +# CSL-04.01 - Specification + source code licenses (dual) +# ----------------------------------------------------------------------------- +[controls."CSL-04.01"] +name = "SpecificationAndCodeLicenses" +description = "The repo clarifies that the spec is under CSL and any code is under MIT/Apache." +tags = { level = 1, domain = "CSL", legal = true, license = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/04-license.md" +help_md = """Clarify the two licenses: the Community Specification License for the +spec, and a source-code license (MIT by default, or Apache-2.0) for any code. + +**Remediation:** create `governance/04-license.md`.""" + +[[controls."CSL-04.01".passes]] +handler = "file_exists" +files = ["governance/04-license.md", "license/04-license.md", "04-license.md"] + +[[controls."CSL-04.01".passes]] +handler = "manual" +steps = ["Confirm governance/04-license.md states CSL for the spec and a code license for source."] + +[controls."CSL-04.01".remediation] +safe = true +dry_run_supported = true + +[[controls."CSL-04.01".remediation.requires_context]] +key = "csl_code_license" +required = false + +[[controls."CSL-04.01".remediation.handlers]] +handler = "file_create" +path = "governance/04-license.md" +template = "csl_license_dual" +overwrite = false + +[controls."CSL-04.01".remediation.project_update] +set = { "legal.csl.licenses.path" = "governance/04-license.md" } + +# ----------------------------------------------------------------------------- +# CSL-05.01 - Governance policy (full CSL, or reference an umbrella) +# ----------------------------------------------------------------------------- +[controls."CSL-05.01"] +name = "GovernancePolicy" +description = "The repo has a governance policy: the full CSL policy, or a reference to an umbrella project's governance." +tags = { level = 1, domain = "CSL", governance = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "governance/05-governance.md" +help_md = """Provide a governance policy. Either adopt the full Community +Specification Governance Policy, or, if the project already follows an umbrella +governance (e.g. CNCF) - reference that and document the CSL roles on top of it. + +**Remediation:** create `governance/05-governance.md` in the chosen mode.""" + +# Under `darnit serve` the LLM judges governance FIRST: is this genuinely CSL +# governance (roles, decision-making, an umbrella reference) rather than a +# passing mention. Under `darnit audit` (no LLM) the sieve stops here +# (PENDING_LLM); the deterministic content/existence passes below answer +# non-LLM callers. +[[controls."CSL-05.01".passes]] +handler = "csl_llm_if_present" +prompt = """Evaluate whether this repository's governance is tied to the +Community Specification process. PASS if it is the full Community Specification +Governance Policy, OR references an umbrella project's governance (e.g. CNCF) +while documenting the Community Specification roles/decision-making. FAIL if the +only governance is generic project governance with no Community Specification +tie, or if no governance file exists.""" +files_to_include = [ + "governance/05-governance.md", + "GOVERNANCE.md", + "governance/GOVERNANCE.md", + "docs/GOVERNANCE.md", +] +confidence_threshold = 0.7 + +# Content detection FIRST: governance must be CSL-specific. Both the full CSL +# policy (title "Community Specification Governance Policy") and the umbrella +# reference stub (which documents the Community Specification process/roles) +# match. A generic GOVERNANCE.md that never mentions Community Specification +# does not, so it FAILs (the email calls for reconciling existing governance). +[[controls."CSL-05.01".passes]] +handler = "regex" +files = [ + "governance/05-governance.md", + "license/05-governance.md", + "05-governance.md", + "GOVERNANCE.md", + "governance/GOVERNANCE.md", + "docs/GOVERNANCE.md", + ".github/GOVERNANCE.md", +] + +[controls."CSL-05.01".passes.pattern.patterns] +csl_governance_content = '(?i)Community Specification (Governance|Process|Roles)' + +# Name-based fallback: conclusive FAIL when no governance file exists at all. +[[controls."CSL-05.01".passes]] +handler = "file_exists" +files = [ + "governance/05-governance.md", + "license/05-governance.md", + "05-governance.md", + "GOVERNANCE.md", + "governance/GOVERNANCE.md", + "docs/GOVERNANCE.md", + ".github/GOVERNANCE.md", +] + +[[controls."CSL-05.01".passes]] +handler = "manual" +steps = ["Confirm a governance policy is present (full CSL policy, or a reference to the umbrella governance with CSL roles documented)."] + +[controls."CSL-05.01".remediation] +safe = true +dry_run_supported = true +strategy = "first_match" + +# Step 4: which governance mode? (optional; defaults to the full CSL policy.) +[[controls."CSL-05.01".remediation.requires_context]] +key = "csl_governance_mode" +required = false +warning = "If the project already follows an umbrella governance (e.g. CNCF), choose 'umbrella' to reference it rather than subsume it." + +[[controls."CSL-05.01".remediation.requires_context]] +key = "csl_governance_reference" +required = false + +# Umbrella mode: reference existing governance (tried first). +[[controls."CSL-05.01".remediation.handlers]] +handler = "file_create" +path = "governance/05-governance.md" +template = "csl_governance_reference" +overwrite = false +when = { csl_governance_mode = "umbrella" } +llm_enhance = "If an umbrella governance was referenced, ensure governance/05-governance.md links it correctly and remove any leftover TODO placeholder. Do not restate the full umbrella policy - only reference it and keep the Community Specification roles and decision-making section." + +# Default: ship the full Community Specification Governance Policy. +[[controls."CSL-05.01".remediation.handlers]] +handler = "file_create" +path = "governance/05-governance.md" +template = "csl_governance" +overwrite = false + +[controls."CSL-05.01".remediation.project_update] +set = { "legal.csl.governance.path" = "governance/05-governance.md" } + +# ----------------------------------------------------------------------------- +# CSL-06.01 - Scope & Notices are discoverable from the README (level 2) +# ----------------------------------------------------------------------------- +[controls."CSL-06.01"] +name = "GovernanceDocsDiscoverable" +description = "The README links the scope and notices files so they are easy to discover." +tags = { level = 2, domain = "CSL", documentation = true } +docs_url = "https://github.com/CommunitySpecification/1.0" +location_hint = "README.md" +help_md = """The LF asks that the scope and notices files be easy to discover. +Reference them (and the /governance directory) from the README. + +**Remediation:** add links to governance/02-scope.md and governance/03-notices.md +in the README.""" + +[[controls."CSL-06.01".passes]] +handler = "regex" +files = ["README.md", "readme.md", "README.rst"] +pass_if_any = false + +[controls."CSL-06.01".passes.pattern.patterns] +links_scope = '(?i)02-scope|/scope|\bscope\.md' +links_notices = '(?i)03-notices|/notices|\bnotices\.md' + +[[controls."CSL-06.01".passes]] +handler = "manual" +steps = [ + "Confirm the README links to the Scope statement (governance/02-scope.md).", + "Confirm the README links to the Notices file (governance/03-notices.md).", +] + +[controls."CSL-06.01".remediation] +safe = true +dry_run_supported = true + +[[controls."CSL-06.01".remediation.handlers]] +handler = "manual" +steps = [ + "Add a short 'Governance & Licensing' section to the README.", + "Link governance/02-scope.md (Scope) and governance/03-notices.md (Notices).", + "Optionally link the /governance directory and governance/04-license.md.", +] diff --git a/packages/darnit-csl/src/darnit_csl/handlers.py b/packages/darnit-csl/src/darnit_csl/handlers.py new file mode 100644 index 00000000..1e6d53e2 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/handlers.py @@ -0,0 +1,55 @@ +"""Custom sieve handlers for the community-spec framework. + +`csl_llm_if_present` gates the LLM content check behind file existence: a missing +file fails deterministically (no reason to consult the model about a file that is +not there), while a file that exists is handed to the LLM for a content-quality +judgment. It is registered in the sieve handler registry with the ``llm`` phase so +the orchestrator's PENDING_LLM branch fires for the present-file case. darnit +imports this package during framework/implementation discovery, so the handler is +available in both the CLI audit path and `darnit serve`. +""" + +from __future__ import annotations + +import os +from typing import Any + +from darnit.sieve.builtin_handlers import llm_eval_handler +from darnit.sieve.handler_registry import ( + HandlerContext, + HandlerResult, + HandlerResultStatus, + get_sieve_handler_registry, +) + + +def csl_llm_if_present(config: dict[str, Any], context: HandlerContext) -> HandlerResult: + """Fail if none of the candidate files exist; otherwise defer to llm_eval. + + Uses the same ``files_to_include`` list as the LLM check, so list every + candidate path for the control. When at least one exists, delegation to + ``llm_eval_handler`` yields the INCONCLUSIVE + consultation result, which the + orchestrator turns into PENDING_LLM (this handler is registered in the ``llm`` + phase). When none exist, this returns a conclusive FAIL and the sieve never + reaches the model. + """ + files = config.get("files_to_include", []) + for f in files: + full = f if os.path.isabs(f) else os.path.join(context.local_path, f) + if os.path.isfile(full): + return llm_eval_handler(config, context) + + return HandlerResult( + status=HandlerResultStatus.FAIL, + message="Required file is missing.", + authority="dispositive", + ) + + +get_sieve_handler_registry().register( + "csl_llm_if_present", + phase="llm", + handler_fn=csl_llm_if_present, + description="Fail if the target file is missing; otherwise defer to the LLM content check.", + default_authority="suggestive", +) diff --git a/packages/darnit-csl/src/darnit_csl/implementation.py b/packages/darnit-csl/src/darnit_csl/implementation.py new file mode 100644 index 00000000..feab9aa6 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/implementation.py @@ -0,0 +1,68 @@ +"""CommunitySpecImplementation — CSL 1.0 compliance, defined in TOML. + +This is a TOML-first ``ComplianceImplementation`` (see the project's +TOML-First Architecture principle): every control, template, and context +prompt lives in ``community-spec.toml``. No Python control logic is needed — +the framework loads the TOML via :meth:`get_framework_config_path`. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +class CommunitySpecImplementation: + """Community Specification License 1.0 compliance implementation.""" + + # ---- Identity ----------------------------------------------------------- + + @property + def name(self) -> str: + """Slug — must match the key in pyproject.toml's + [project.entry-points."darnit.implementations"] table.""" + return "community-spec" + + @property + def display_name(self) -> str: + return "Community Specification License (CSL 1.0)" + + @property + def version(self) -> str: + return "0.1.0" + + @property + def spec_version(self) -> str: + return "CSL 1.0" + + # ---- Protocol methods --------------------------------------------------- + + def get_framework_config_path(self) -> Path | None: + """Absolute path to the bundled TOML config (the source of truth).""" + from importlib.resources import files + + resource = files(__package__) / "community-spec.toml" + path = Path(str(resource)) + if not path.is_file(): + raise FileNotFoundError( + f"community-spec.toml not found in installed darnit_csl package " + f"at {path}. This indicates a broken build; check the wheel's " + f"force-include configuration." + ) + return path + + def register_controls(self) -> None: + """No Python-registered controls — everything is in the TOML.""" + return None + + def get_all_controls(self) -> list[Any]: + return [] + + def get_controls_by_level(self, level: int) -> list[Any]: + return [] + + def get_rules_catalog(self) -> dict[str, Any]: + return {} + + def get_remediation_registry(self) -> dict[str, Any]: + return {} diff --git a/packages/darnit-csl/src/darnit_csl/mcp_tools.py b/packages/darnit-csl/src/darnit_csl/mcp_tools.py new file mode 100644 index 00000000..5463da53 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/mcp_tools.py @@ -0,0 +1,199 @@ +"""MCP tools for the community-spec framework.""" + +from __future__ import annotations + +import re +from pathlib import Path + +from darnit.core.logging import get_logger + +logger = get_logger("darnit_csl.mcp_tools") + +_PLACEHOLDER_RE = re.compile( + r"(?i)(real name|real@email|example\.(com|org|net)|\bTODO\b|\bFIXME\b|\bTBD\b" + r"|||your name|firstname|lastname|insert name|name one|name two" + r"|john doe|jane doe|_{5,}|\[Ideally list two different individuals)" +) + + +_MAILING_LIST_RE = re.compile( + r"(?i)(@googlegroups\.com|@groups\.|@lists?\.|[._-]lists?@|listserv|majordomo" + r"|\bno-?reply\b|@.*\.(?:groups|mailman)\b)" +) + + +def _looks_like_mailing_list(value: str) -> bool: + """True if the contact is a group address rather than a named individual.""" + return bool(_MAILING_LIST_RE.search(value)) + + +def _looks_like_placeholder(value: str) -> bool: + """True if the value is empty or still looks like unfilled template text.""" + return not value.strip() or bool(_PLACEHOLDER_RE.search(value)) + + +_URL_RE = re.compile(r"https?://") + +_COC_CANDIDATES = ( + "CODE_OF_CONDUCT.md", + "CODE-OF-CONDUCT.md", + "code-of-conduct.md", + "code_of_conduct.md", + ".github/CODE_OF_CONDUCT.md", + ".github/CODE-OF-CONDUCT.md", + "docs/CODE_OF_CONDUCT.md", + "docs/CODE-OF-CONDUCT.md", +) + + +def _find_existing_coc(repo: Path) -> str | None: + """Relative path of an existing Code of Conduct file in the repo, if any.""" + for rel in _COC_CANDIDATES: + if (repo / rel).is_file(): + return rel + return None + + +_ORDER = ["CSL-01.01", "CSL-01.02", "CSL-02.01", "CSL-03.01", "CSL-04.01", "CSL-05.01"] + + +async def remediate_community_spec( + local_path: str, + scope: str = "", + coc_contacts: str = "", + code_license: str = "MIT", + governance_mode: str = "csl", + governance_reference: str = "", + coc_policy: str = "csl", + coc_reference: str = "", + spec_name: str = "", + add_readme_links: bool = True, +) -> str: + """Write the Community Specification License (CSL 1.0) file set into a repo.""" + from darnit.config import load_framework_config + from darnit.remediation.executor import RemediationExecutor + from darnit.tools.audit import run_sieve_audit + + repo = Path(local_path).resolve() + if not repo.exists(): + return f"Error: repository path not found: {repo}" + + coc_policy = (coc_policy or "csl").strip().lower() + + # Org-first: a project that already has a Code of Conduct (its own file, + # an org community repo, or a foundation CoC such as CNCF / LF / JDF) + # must point at it rather than naming individuals. + existing_coc = _find_existing_coc(repo) + if coc_policy != "org" and existing_coc: + return ( + f"Error: this repository already has a Code of Conduct at " + f"'{existing_coc}'. Re-run with coc_policy='org' and coc_reference " + "set to one markdown sentence linking that document. Nothing was " + "written." + ) + + if coc_policy == "org": + if _looks_like_placeholder(coc_reference) or not _URL_RE.search(coc_reference): + return ( + "Error: coc_policy='org' requires coc_reference: one markdown " + "sentence linking the project's existing Code of Conduct (the " + "repo's own file, the org's community repo, or a foundation " + "CoC such as CNCF / LF / JDF). Check those locations first; " + "only fall back to named individuals when no such CoC exists. " + "Nothing was written." + ) + # The linked CoC defines the reporting procedure; no inline contacts. + coc_contacts = "" + else: + if _looks_like_placeholder(coc_contacts): + return ( + f"Error: coc_contacts looks like a placeholder ({coc_contacts!r}). " + "CSL requires a real Code of Conduct contact, ideally two named " + "individuals with emails. Nothing was written." + ) + + if _looks_like_mailing_list(coc_contacts): + return ( + f"Error: coc_contacts is a group address ({coc_contacts!r}). CSL asks " + "for named individuals rather than a generic mailing list, so that " + "someone filing a complaint knows exactly who receives it. Ask the " + "project who should be listed. Nothing was written." + ) + + # The csl_scope template supplies the "# Scope" heading and the closing + # "Any changes of Scope are not retroactive." line. A caller that drafts a + # full scope document will include both, so strip them here rather than + # emitting each one twice. + scope = re.sub(r"\A#\s*Scope\s*\n+", "", scope.strip()) + scope = re.sub( + r"\n*Any changes of Scope are not retroactive\.\s*\Z", "", scope + ).strip() + + fw_path = Path(__file__).parent / "community-spec.toml" + fw = load_framework_config(fw_path) + + context_values = { + "csl_spec_name": spec_name, + "csl_working_group_scope": scope, + "csl_coc_contacts": coc_contacts, + "csl_code_license": code_license, + "csl_governance_mode": governance_mode, + "csl_governance_reference": governance_reference, + "csl_coc_policy": coc_policy, + "csl_coc_reference": coc_reference, + } + + executor = RemediationExecutor( + local_path=str(repo), owner="", repo="", + templates=fw.templates, context_values=context_values, + framework_path=str(fw_path), + ) + + written: list[str] = [] + errors: list[str] = [] + for cid in _ORDER: + control = fw.controls.get(cid) + if control is None or control.remediation is None: + continue + try: + res = executor.execute(cid, control.remediation, dry_run=False) + (written if res.success else errors).append( + cid if res.success else f"{cid}: {res.message}" + ) + except Exception as e: # noqa: BLE001 + errors.append(f"{cid}: {e}") + + if add_readme_links: + candidates = ["README.md", "README.rst", "readme.md", "Readme.md", "README.txt"] + readme = next((repo / c for c in candidates if (repo / c).is_file()), None) + created = readme is None + if created: + readme = repo / "README.md" + text = "# Specification\n" if created else readme.read_text(encoding="utf-8") + if "02-scope.md" not in text: + if readme.suffix.lower() == ".rst": + block = ("\nGovernance & Licensing\n----------------------\n\n" + "- `Scope `_\n" + "- `Notices `_\n" + "- `License `_\n" + "- `Governance `_\n") + else: + block = ("\n## Governance & Licensing\n" + "- [Scope](governance/02-scope.md)\n" + "- [Notices](governance/03-notices.md)\n" + "- [License](governance/04-license.md)\n" + "- [Governance](governance/05-governance.md)\n") + readme.write_text(text + block, encoding="utf-8") + written.append(f"CSL-06.01 ({readme.name} links)") + + results, _ = run_sieve_audit( + owner="", repo="", local_path=str(repo), default_branch="main", + apply_user_config=False, framework_name="community-spec", stop_on_llm=False, + ) + lines = [f"# CSL remediation for {repo.name}", "", + f"Files written: {', '.join(written) if written else 'none'}"] + if errors: + lines += ["", "Errors:"] + [f"- {e}" for e in errors] + lines += ["", "## Re-audit"] + lines += [f"- {r.get('id')}: {r.get('status')}" for r in sorted(results, key=lambda r: r.get("id", ""))] + return "\n".join(lines) diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_cla.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_cla.tmpl new file mode 100644 index 00000000..46419101 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_cla.tmpl @@ -0,0 +1,18 @@ +# Community Specification Contributor License Agreement 1.0 + +By making a Contribution to this repository, I agree to the terms of the following documents located at [https://github.com/CommunitySpecification/1.0](https://github.com/CommunitySpecification/1.0): + +(a) Community Specification License 1.0 ([01-community-specification-license-v1.md](https://github.com/CommunitySpecification/1.0/blob/main/01-community-specification-license-v1.md)) + +(b) Community Specification Governance Policy 1.0 ([05-governance.md](https://github.com/CommunitySpecification/1.0/blob/main/05-governance.md)) + +(c) Community Specification Contribution Policy 1.0 ([06-contributing.md](https://github.com/CommunitySpecification/1.0/blob/main/06-contributing.md)) + +(d) Community Specification Code of Conduct ([08-code-of-conduct.md](https://github.com/CommunitySpecification/1.0/blob/main/08-code-of-conduct.md)) + + +In addition, for source code contributions, I certify that: + +(a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this working group and the contribution may be public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this agreement or the open source license(s) involved. + +I represent that I am legally entitled to make the grants set forth in the documents above. If my employer(s) has rights to intellectual property that may be infringed by the materials developed by this Working Group, I represent that I have received permission to enter these agreements on behalf of that employer. diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_governance.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_governance.tmpl new file mode 100644 index 00000000..11494b2a --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_governance.tmpl @@ -0,0 +1,51 @@ +# Community Specification Governance Policy 1.0 + +This document provides the governance policy for specifications and other documents developed using the Community Specification process in a repository (each a "Working Group"). Each Working Group and must adhere to the requirements in this document. + +## 1. Roles. + +Each Working Group may include the following roles. Additional roles may be adopted and documented by the Working Group. + +**1.1. Maintainer.** "Maintainers" are responsible for organizing activities around developing, maintaining, and updating the specification(s) developed by the Working Group. Maintainers are also responsible for determining consensus and coordinating appeals. Each Working Group will designate one or more Maintainer for that Working Group. A Working Group may select a new or additional Maintainer(s) upon Approval of the Working Group Participants. + +**1.2. Editor.** "Editors" are responsible for ensuring that the contents of the document accurately reflect the decisions that have been made by the group, and that the specification adheres to formatting and content guidelines. Each Working Group will designate an Editor for that Working Group. A Working Group may select a new Editor upon Approval of the Working Group Participants. + +**1.3. Participants.** "Participants" are those that have made Contributions to the Working Group subject to the Community Specification License. + +## 2. Decision Making. + +**2.1. Consensus-Based Decision Making.** Working Groups make decisions through a consensus process ("Approval" or "Approved"). While the agreement of all Participants is preferred, it is not required for consensus. Rather, the Maintainer will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Working Group Participants and nature of support and objections. The Maintainer will document evidence of consensus in accordance with these requirements. + +**2.2. Appeal Process.** Decisions may be appealed be via a pull request or an issue, and that appeal will be considered by the Maintainer in good faith, who will respond in writing within a reasonable time. + +## 3. Ways of Working. + +Inspired by [ANSI's Essential Requirements for Due Process](https://share.ansi.org/Shared%20Documents/Standards%20Activities/American%20National%20Standards/Procedures,%20Guides,%20and%20Forms/2020_ANSI_Essential_Requirements.pdf), Community Specification Working Groups must adhere to consensus-based due process requirements. These requirements apply to activities related to the development of consensus for approval, revision, reaffirmation, and withdrawal of Community Specifications. Due process means that any person (organization, company, government agency, individual, etc.) with a direct and material interest has a right to participate by: a) expressing a position and its basis, b) having that position considered, and c) having the right to appeal. Due process allows for equity and fair play. The following constitute the minimum acceptable due process requirements for the development of consensus. + +**3.1. Openness.** Participation shall be open to all persons who are directly and materially affected by the activity in question. There shall be no undue financial barriers to participation. Voting membership on the consensus body shall not be conditional upon membership in any organization, nor unreasonably restricted on the basis of technical qualifications or other such requirements. Membership in a Working Group's parent organization, if any, may be required. + +**3.2. Lack of Dominance.** The development process shall not be dominated by any single interest category, individual or organization. Dominance means a position or exercise of dominant authority, leadership, or influence by reason of superior leverage, strength, or representation to the exclusion of fair and equitable consideration of other viewpoints. + +**3.3. Balance.** The development process should have a balance of interests. Participants from diverse interest categories shall be sought with the objective of achieving balance. + +**3.4. Coordination and Harmonization.** Good faith efforts shall be made to resolve potential conflicts between and among deliverables developed under this Working Group and existing industry standards. + +**3.5. Consideration of Views and Objections.** Prompt consideration shall be given to the written views and objections of all Participants. + +**3.6. Written procedures.** This governance document and other materials documenting the Community Specification development process shall be available to any interested person. + +## 4. Specification Development Process. + +**4.1. Pre-Draft.** Any Participant may submit a proposed initial draft document as a candidate Draft Specification of that Working Group. The Maintainer will designate each submission as a "Pre-Draft" document. + +**4.2. Draft.** Each Pre-Draft document of a Working Group must first be Approved to become a" Draft Specification". Once the Working Group approves a document as a Draft Specification, the Draft Specification becomes the basis for all going forward work on that specification. + +**4.3. Working Group Approval.** Once a Working Group believes it has achieved the objectives for its specification as described in the Scope, it will Approve that Draft Specification and progress it to "Approved Specification" status. + +**4.4. Publication and Submission.** Upon the designation of a Draft Specification as an Approved Specification, the Maintainer will publish the Approved Specification in a manner agreed upon by the Working Group Participants (i.e., Working Group Participant only location, publicly available location, Working Group maintained website, Working Group member website, etc.). The publication of an Approved Specification in a publicly accessible manner must include the terms under which the Approved Specification is being made available under. + +**4.5. Submissions to Standards Bodies.** No Draft Specification or Approved Specification may be submitted to another standards development organization without Working group Approval. Upon reaching Approval, the Maintainer will coordinate the submission of the applicable Draft Specification or Approved Specification to another standards development organization. Working Group Participants that developed that Draft Specification or Approved Specification agree to grant the copyright rights necessary to make those submissions. + +## 5. Non-Confidential, Restricted Disclosure. + +Information disclosed in connection with any Working Group activity, including but not limited to meetings, Contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. Notwithstanding the foregoing, if the Working Group is collaborating via a private repository, the Participants will not make any public disclosures of that information contained in that private repository without the Approval of the Working Group. diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_governance_reference.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_governance_reference.tmpl new file mode 100644 index 00000000..00a39e14 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_governance_reference.tmpl @@ -0,0 +1,19 @@ +# Governance + +This Working Group develops specifications using the Community Specification process. + +## Adopted Governance + +The overall governance for this project is provided by << context.csl_governance_reference | default('[the umbrella project governance - TODO: name and link the adopted GOVERNANCE policy, e.g. the CNCF Charter / project governance]') >>. That governance applies to this repository and is **not superseded** by this file. Where the adopted governance and the Community Specification process overlap, the adopted governance controls. + +## Community Specification Roles and Decision-Making + +For developing the specification(s) in this repository, the following Community Specification roles and consensus process apply, consistent with the adopted governance above: + +- **Maintainer(s)** organize activities around developing, maintaining, and updating the specification(s); determine consensus; and coordinate appeals. +- **Editor(s)** ensure the document accurately reflects the group's decisions and adheres to formatting and content guidelines. +- **Participants** are those that have made Contributions to the Working Group subject to the Community Specification License. + +Decisions are made through a consensus process. Appeals may be raised via a pull request or an issue and are considered by the Maintainer in good faith, who responds in writing within a reasonable time. + +The full Community Specification Governance Policy 1.0 is available at [https://github.com/CommunitySpecification/1.0](https://github.com/CommunitySpecification/1.0) and may be referenced for any specification-development process not addressed by the adopted governance above. diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_license.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_license.tmpl new file mode 100644 index 00000000..d2180921 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_license.tmpl @@ -0,0 +1,99 @@ +# Community Specification License 1.0 + +**The Purpose of this License.** This License sets forth the terms under which 1) Contributor will participate in and contribute to the development of specifications, standards, best practices, guidelines, and other similar materials under this Working Group, and 2) how the materials developed under this License may be used. It is not intended for source code. Capitalized terms are defined in the License's last section. + +**1. Copyright.** + +**1.1. Copyright License.** Contributor grants everyone a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) copyright license, without any obligation for accounting, to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute any materials it submits to the full extent of its copyright interest in those materials. Contributor also acknowledges that the Working Group may exercise copyright rights in the Specification, including the rights to submit the Specification to another standards organization. + +**1.2. Copyright Attribution.** As a condition, anyone exercising this copyright license must include attribution to the Working Group in any derivative work based on materials developed by the Working Group. That attribution must include, at minimum, the material's name, version number, and source from where the materials were retrieved. Attribution is not required for implementations of the Specification. + +**2. Patents.** + +**2.1. Patent License.** + +**2.1.1. As a Result of Contributions.** + +**2.1.1.1. As a Result of Contributions to Draft Specifications.** Contributor grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims in 1) Contributor's Contributions and 2) to the Draft Specification that is within Scope as of the date of that Contribution, in both cases for Licensee's Implementation of the Draft Specification, except for those patent claims excluded by Contributor under Section 3. + +**2.1.1.2. For Approved Specifications.** Contributor grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims included in the Approved Specification that are within Scope for Licensee's Implementation of the Approved Specification, except for those patent claims excluded by Contributor under Section 3. + +**2.1.2. Patent Grant from Licensee.** Licensee grants each other Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as expressly stated in this License) license to its Necessary Claims for its Implementation, except for those patent claims excluded under Section 3. + +**2.1.3. Licensee Acceptance.** The patent grants set forth in Section 2.1 extend only to Licensees that have indicated their agreement to this License as follows: + +**2.1.3.1. Source Code Distributions.** For distribution in source code, by including this License in the root directory of the source code with the Implementation; + +**2.1.3.2. Non-Source Code Distributions.** For distribution in any form other than source code, by including this License in the documentation, legal notices, via notice in the software, and/or other written materials provided with the Implementation; or + +**2.1.3.3. Via Notices.md.** By issuing pull request or commit to the Specification's repository's Notices.md file by the Implementer's authorized representative, including the Implementer's name, authorized individual and system identifier, and Specification version. + +**2.1.4. Defensive Termination.** If any Licensee files or maintains a claim in a court asserting that a Necessary Claim is infringed by an Implementation, any licenses granted under this License to the Licensee are immediately terminated unless 1) that claim is directly in response to a claim against Licensee regarding an Implementation, or 2) that claim was brought to enforce the terms of this License, including intervention in a third-party action by a Licensee. + +**2.1.5. Additional Conditions.** This License is not an assurance (i) that any of Contributor's copyrights or issued patent claims cover an Implementation of the Specification or are enforceable or (ii) that an Implementation of the Specification would not infringe intellectual property rights of any third party. + +**2.2. Patent Licensing Commitment.** In addition to the rights granted in Section 2.1, Contributor agrees to grant everyone a no charge, royalty-free license on reasonable and non-discriminatory terms to Contributor's Necessary Claims that are within Scope for: +1) Implementations of a Draft Specification, where such license applies only to those Necessary Claims infringed by implementing Contributor's Contribution(s) included in that Draft Specification, and +2) Implementations of the Approved Specification. + +This patent licensing commitment does not apply to those claims subject to Contributor's Exclusion Notice under Section 3. + +**2.3. Effect of Withdrawal.** Contributor may withdraw from the Working Group by issuing a pull request or commit providing notice of withdrawal to the Working Group repository's Notices.md file. All of Contributor's existing commitments and obligations with respect to the Working Group up to the date of that withdrawal notice will remain in effect, but no new obligations will be incurred. + +**2.4. Binding Encumbrance.** This License is binding on any future owner, assignee, or party who has been given the right to enforce any Necessary Claims against third parties. + +**3. Patent Exclusion.** + +**3.1. As a Result of Contributions.** Contributor may exclude Necessary Claims from its licensing commitments incurred under Section 2.1.1 by issuing an Exclusion Notice within 45 days of the date of that Contribution. Contributor may not issue an Exclusion Notice for any material that has been included in a Draft Deliverable for more than 45 days prior to the date of that Contribution. + +**3.2. As a Result of a Draft Specification Becoming an Approved Specification.** Prior to the adoption of a Draft Specification as an Approved Specification, Contributor may exclude Necessary Claims from its licensing commitments under this Agreement by issuing an Exclusion Notice. Contributor may not issue an Exclusion Notice for patents that were eligible to have been excluded pursuant to Section 3.1. + +**4. Source Code License.** Any source code developed by the Working Group is solely subject the source code license included in the Working Group's repository for that code. If no source code license is included, the source code will be subject to the MIT License. + +**5. No Other Rights.** Except as specifically set forth in this License, no other express or implied patent, trademark, copyright, or other rights are granted under this License, including by implication, waiver, or estoppel. + +**6. Antitrust Compliance.** Contributor acknowledge that it may compete with other participants in various lines of business and that it is therefore imperative that they and their respective representatives act in a manner that does not violate any applicable antitrust laws and regulations. This License does not restrict any Contributor from engaging in similar specification development projects. Each Contributor may design, develop, manufacture, acquire or market competitive deliverables, products, and services, and conduct its business, in whatever way it chooses. No Contributor is obligated to announce or market any products or services. Without limiting the generality of the foregoing, the Contributors agree not to have any discussion relating to any product pricing, methods or channels of product distribution, division of markets, allocation of customers or any other topic that should not be discussed among competitors under the auspices of the Working Group. + +**7. Non-Circumvention.** Contributor agrees that it will not intentionally take or willfully assist any third party to take any action for the purpose of circumventing any obligations under this License. + +**8. Representations, Warranties and Disclaimers.** + +**8.1. Representations, Warranties and Disclaimers.** Contributor and Licensee represents and warrants that 1) it is legally entitled to grant the rights set forth in this License and 2) it will not intentionally include any third party materials in any Contribution unless those materials are available under terms that do not conflict with this License. IN ALL OTHER RESPECTS ITS CONTRIBUTIONS ARE PROVIDED "AS IS." The entire risk as to implementing or otherwise using the Contribution or the Specification is assumed by the implementer and user. Except as stated herein, CONTRIBUTOR AND LICENSEE EXPRESSLY DISCLAIM ANY WARRANTIES (EXPRESS, IMPLIED, OR OTHERWISE), INCLUDING IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, FITNESS FOR A PARTICULAR PURPOSE, CONDITIONS OF QUALITY, OR TITLE, RELATED TO THE CONTRIBUTION OR THE SPECIFICATION. IN NO EVENT WILL ANY PARTY BE LIABLE TO ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND WITH RESPECT TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Any obligations regarding the transfer, successors in interest, or assignment of Necessary Claims will be satisfied if Contributor or Licensee notifies the transferee or assignee of any patent that it knows contains Necessary Claims or necessary claims under this License. Nothing in this License requires Contributor to undertake a patent search. If Contributor is 1) employed by or acting on behalf of an employer, 2) is making a Contribution under the direction or control of a third party, or 3) is making the Contribution as a consultant, contractor, or under another similar relationship with a third party, Contributor represents that they have been authorized by that party to enter into this License on its behalf. + +**8.2. Distribution Disclaimer.** Any distributions of technical information to third parties must include a notice materially similar to the following: "THESE MATERIALS ARE PROVIDED "AS IS." The Contributors and Licensees expressly disclaim any warranties (express, implied, or otherwise), including implied warranties of merchantability, non-infringement, fitness for a particular purpose, or title, related to the materials. The entire risk as to implementing or otherwise using the materials is assumed by the implementer and user. IN NO EVENT WILL THE CONTRIBUTORS OR LICENSEES BE LIABLE TO ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND WITH RESPECT TO THIS DELIVERABLE OR ITS GOVERNING AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR NOT THE OTHER MEMBER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +**9. Definitions.** + +**9.1. Affiliate.** "Affiliate" means an entity that directly or indirectly Controls, is Controlled by, or is under common Control of that party. + +**9.2. Approved Specification.** "Approved Specification" means the final version and contents of any Draft Specification designated as an Approved Specification as set forth in the accompanying Governance.md file. + +**9.3. Contribution.** "Contribution" means any original work of authorship, including any modifications or additions to an existing work, that Contributor submits for inclusion in a Draft Specification, which is included in a Draft Specification or Approved Specification. + +**9.4. Contributor.** "Contributor" means any person or entity that has indicated its acceptance of the License 1) by making a Contribution to the Specification, or 2) by entering into the Community Specification Contributor License Agreement for the Specification. Contributor includes its Affiliates, assigns, agents, and successors in interest. + +**9.5. Control.** "Control" means direct or indirect control of more than 50% of the voting power to elect directors of that corporation, or for any other entity, the power to direct management of such entity. + +**9.6. Draft Specification.** "Draft Specification" means all versions of the material (except an Approved Specification) developed by this Working Group for the purpose of creating, commenting on, revising, updating, modifying, or adding to any document that is to be considered for inclusion in the Approved Specification. + +**9.7. Exclusion Notice.** "Exclusion Notice" means a written notice made by making a pull request or commit to the repository's Notices.md file that identifies patents that Contributor is excluding from its patent licensing commitments under this License. The Exclusion Notice for issued patents and published applications must include the Draft Specification's name, patent number(s) or title and application number(s), as the case may be, for each of the issued patent(s) or pending patent application(s) that the Contributor is excluding from the royalty-free licensing commitment set forth in this License. If an issued patent or pending patent application that may contain Necessary Claims is not set forth in the Exclusion Notice, those Necessary Claims shall continue to be subject to the licensing commitments under this License. The Exclusion Notice for unpublished patent applications must provide either: (i) the text of the filed application; or (ii) identification of the specific part(s) of the Draft Specification whose implementation makes the excluded claim a Necessary Claim. If (ii) is chosen, the effect of the exclusion will be limited to the identified part(s) of the Draft Specification. + +**9.8. Implementation.** "Implementation" means making, using, selling, offering for sale, importing or distributing any implementation of the Specification 1) only to the extent it implements the Specification and 2) so long as all required portions of the Specification are implemented. + +**9.9. License.** "License" means this Community Specification License. + +**9.10. Licensee.** "Licensee" means any person or entity that has indicated its acceptance of the License as set forth in Section 2.1.3. Licensee includes its Affiliates, assigns, agents, and successors in interest. + +**9.11. Necessary Claims.** "Necessary Claims" are those patent claims, if any, that a party owns or controls, including those claims later acquired, that are necessary to implement the required portions (including the required elements of optional portions) of the Specification that are described in detail and not merely referenced in the Specification. + +**9.12. Specification.** "Specification" means a Draft Specification or Approved Specification included in the Working Group's repository subject to this License, and the version of the Specification implemented by the Licensee. + +**9.13. Scope.** "Scope" has the meaning as set forth in the accompanying Scope.md file included in this Specification's repository. Changes to Scope do not apply retroactively. If no Scope is provided, each Contributor's Necessary Claims are limited to that Contributor's Contributions. + +**9.14. Working Group.** "Working Group" means this project to develop specifications, standards, best practices, guidelines, and other similar materials under this License. + + + +*The text of this Community Specification License is Copyright 2020 Joint Development Foundation and is licensed under the Creative Commons Attribution 4.0 International License available at https://creativecommons.org/licenses/by/4.0/.* + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_license_dual.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_license_dual.tmpl new file mode 100644 index 00000000..e197ffc2 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_license_dual.tmpl @@ -0,0 +1,11 @@ +# Licenses + +## Specification License + +Specifications in this repository are subject to the **Community Specification License 1.0** available at [https://github.com/CommunitySpecification/1.0](https://github.com/CommunitySpecification/1.0). + +## Source Code License + +If source code is included in this repository, or for sample or reference code included in the specification itself, that code is subject to the << context.csl_code_license | default('MIT') >> license unless otherwise marked. + +In the case of any conflict or confusion within this specification repository between the Community Specification License and the designated source code license, the terms of the Community Specification License shall apply. diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_notices.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_notices.tmpl new file mode 100644 index 00000000..cf8ff762 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_notices.tmpl @@ -0,0 +1,56 @@ +# Notices + +## Code of Conduct + +<< context.csl_coc_reference if (context.csl_coc_policy == 'org' and context.csl_coc_reference) else ((('This Working Group follows the ' ~ context.csl_coc_reference ~ ' for Code of Conduct matters.\n\n') if (context.csl_coc_policy == 'umbrella' and context.csl_coc_reference) else '') ~ 'Contact for Code of Conduct issues or inquiries: ' ~ (context.csl_coc_contacts | default('_________________'))) >> + + +## License Acceptance + +Per Community Specification License 1.0 Section 2.1.3.3, Licensees may indicate their acceptance of the Community Specification License by issuing a pull request to the Specification's repository's Notice.md file, including the Licensee's name, authorized individuals' names, and repository system identifier (e.g. GitHub ID), and specification version. + +A Licensee may consent to accepting the current Community Specification License version or any future version of the Community Specification License by indicating "or later" after their specification version. + +--------------------------------------------------------------------------------- + +Licensee's name: + +Authorized individual and system identifier: + +Specification version: + +--------------------------------------------------------------------------------- + +## Withdrawals + +Name of party withdrawing: + +Date of withdrawal: + +--------------------------------------------------------------------------------- + +## Exclusions + +This section includes any Exclusion Notices made against a Draft Deliverable or Approved Deliverable as set forth in the Community Specification Development License. Each Exclusion Notice must include the following information: + +- Name of party making the Exclusion Notice: + +- Name of patent owner: + +- Specification: + +- Version number: + +**For issued patents and published patent applications:** + + (i) patent number(s) or title and application number(s), as the case may be: + + (ii) identification of the specific part(s) of the Specification whose implementation makes the excluded claim a Necessary Claim. + +**For unpublished patent applications must provide either:** + + (i) the text of the filed application; or + + (ii) identification of the specific part(s) of the Specification whose implementation makes the excluded claim a Necessary Claim. + +----------------------------------------------------------------------------------------- diff --git a/packages/darnit-csl/src/darnit_csl/templates/csl_scope.tmpl b/packages/darnit-csl/src/darnit_csl/templates/csl_scope.tmpl new file mode 100644 index 00000000..8fc58561 --- /dev/null +++ b/packages/darnit-csl/src/darnit_csl/templates/csl_scope.tmpl @@ -0,0 +1,5 @@ +# Scope + +<< context.csl_working_group_scope | default("[Include a detailed description of this Working Group's Scope. This Scope is important as it establishes the bounds of each contributor's and licensee's patent commitment. For guidance on drafting an appropriate Scope, you may find ISO's guidance (see page 5) helpful: https://www.iso.org/files/live/sites/isoorg/files/developing_standards/docs/en/how-to-write-standards.pdf]") >> + +Any changes of Scope are not retroactive. diff --git a/packages/darnit/src/darnit/server/registry.py b/packages/darnit/src/darnit/server/registry.py index 891b4722..55917ed9 100644 --- a/packages/darnit/src/darnit/server/registry.py +++ b/packages/darnit/src/darnit/server/registry.py @@ -195,6 +195,21 @@ async def bound_handler(**kwargs): kwargs["_framework_name"] = framework_name return await base_fn(**kwargs) + # functools.wraps makes inspect.signature follow __wrapped__ back to + # base_fn, which still lists the injected `_framework_name`. Newer + # FastMCP rejects any tool parameter starting with '_', so strip it + # from the exposed signature (same approach as _bind_tool_config). + import inspect + + sig = inspect.signature(base_fn) + bound_handler.__signature__ = sig.replace( + parameters=[ + p + for name, p in sig.parameters.items() + if name != "_framework_name" + ] + ) + return bound_handler def get_tool(self, name: str) -> ToolSpec | None: diff --git a/pyproject.toml b/pyproject.toml index 08742cde..4765d9d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ darnit-gittuf = { workspace = true } darnit-reproducibility = { workspace = true } darnit-hello = { workspace = true } darnit-testchecks = { workspace = true } +darnit-csl = { workspace = true } [tool.pytest.ini_options] testpaths = ["tests"] @@ -120,6 +121,7 @@ dev = [ "darnit-example", "darnit-hello", "darnit-testchecks", # framework used by tests/darnit/cli/ (feature 024) + "darnit-csl", # optional CSL 1.0 framework; exercised by tests/darnit_csl ] [tool.mypy] diff --git a/scripts/csl_manual_remediate.py b/scripts/csl_manual_remediate.py new file mode 100644 index 00000000..5c3618e7 --- /dev/null +++ b/scripts/csl_manual_remediate.py @@ -0,0 +1,87 @@ +"""Manually drive CSL remediation against a repo, then re-audit. + +Run from the darnit repo root: + + uv run python scripts/csl_manual_remediate.py + +This is the deterministic (no-LLM) path. It shows steps 5-7 of the pattern: + 5. project_update writes paths back to .project + 6. file_create renders each file from its template + 7. the [+llm_enhance] tag marks files an LLM would refine in the MCP flow + +Edit ANSWERS to experiment (governance mode, code license, CoC mode). +""" + +import sys +from pathlib import Path + +from darnit.config import load_framework_config +from darnit.remediation.executor import RemediationExecutor +from darnit.tools.audit import run_sieve_audit + +FW = Path("packages/darnit-csl/src/darnit_csl/community-spec.toml") + +ANSWERS = { + "csl_working_group_scope": ( + "This Working Group standardizes the Foo attestation metadata format " + "and the rules for verifying Foo attestations across implementations." + ), + "csl_coc_contacts": "Jane Doe (jane@example.org), John Roe (@johnroe)", + "csl_code_license": "MIT", # try "Apache-2.0" + "csl_governance_mode": "csl", # try "umbrella" + "csl_governance_reference": "CNCF project governance (https://github.com/cncf/foundation/blob/main/charter.md)", + "csl_coc_policy": "csl", # try "umbrella" + "csl_coc_reference": "CNCF Code of Conduct (https://github.com/cncf/foundation/blob/main/code-of-conduct.md)", +} + +ORDER = ["CSL-01.01", "CSL-01.02", "CSL-02.01", "CSL-03.01", "CSL-04.01", "CSL-05.01", "CSL-06.01"] + + +def main(target: str) -> None: + fw = load_framework_config(FW) + ex = RemediationExecutor( + local_path=target, + owner="example-org", + repo="spec-repo", + templates=fw.templates, + context_values=ANSWERS, + framework_path=str(FW), + ) + + print("== remediation ==") + for cid in ORDER: + res = ex.execute(cid, fw.controls[cid].remediation, dry_run=False) + tag = " [+llm_enhance]" if any("llm_enhance" in h for h in res.details.get("handlers", [])) else "" + pu = res.details.get("project_update", "") + pu = f" project_update={pu}" if pu else "" + print(f" {cid}: success={res.success} :: {res.message}{tag}{pu}") + + # CSL-06 is a manual control (link the docs from the README). Simulate it so + # the re-audit is fully green. + readme = Path(target) / "README.md" + text = readme.read_text(encoding="utf-8") if readme.exists() else "# Spec\n" + if "02-scope.md" not in text: + text += "\n## Governance & Licensing\n- [Scope](governance/02-scope.md)\n- [Notices](governance/03-notices.md)\n" + readme.write_text(text, encoding="utf-8") + print(" CSL-06.01: applied manual step (added README links)") + + print("\n== generated files ==") + for f in sorted(Path(target).rglob("*.md")): + print(f" {f.relative_to(target)} ({f.stat().st_size} b)") + + print("\n== re-audit ==") + results, _ = run_sieve_audit( + owner="o", repo="r", local_path=target, + default_branch="main", apply_user_config=False, + framework_name="community-spec", stop_on_llm=False, + ) + for r in sorted(results, key=lambda r: r["id"]): + mark = "PASS" if r["status"] == "PASS" else r["status"] + print(f" {r['id']}: {mark}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: uv run python scripts/csl_manual_remediate.py ") + raise SystemExit(2) + main(sys.argv[1]) diff --git a/scripts/csl_onboard.py b/scripts/csl_onboard.py new file mode 100644 index 00000000..cc615b09 --- /dev/null +++ b/scripts/csl_onboard.py @@ -0,0 +1,197 @@ +"""One-command CSL 1.0 onboarding for a specification repository. + +Runs the whole per-repo sequence: clone, branch, baseline audit, remediate from a +drafted scope file, placeholder scan, final audit, and print the PR commands. + +Typical use (two steps per repo): + + # 1. see where the repo stands, and get a scope template to fill in + uv run python scripts/csl_onboard.py --repo uptane/uptane-standard --audit-only + + # 2. after editing the scope file, do the real run + uv run python scripts/csl_onboard.py --repo uptane/uptane-standard \ + --scope-file scopes/uptane.md \ + --contacts "Name One , Name Two " \ + --code-license Apache-2.0 --spec-name "Uptane" + +The scope is passed as a FILE, not a command-line string, so long prose does not +have to survive shell quoting. Nothing is written unless --scope-file is given. +""" + +from __future__ import annotations + +import argparse +import asyncio +import re +import subprocess +import sys +from pathlib import Path + +# Repo names that are ambiguous on their own (several orgs use them), so the +# local directory and the fork get an owner prefix. +_GENERIC = {"specification", "spec", "standard", "docs"} + +_SCOPE_TEMPLATE = """[One paragraph: what this Working Group standardizes.] + +## In Scope + +* **[Component]:** [what it defines] + +## Out of Scope + +* **[Area]:** [why it is excluded] +""" + + +def sh(args: list[str], cwd: Path | None = None, check: bool = True) -> str: + """Run a command and return stdout, echoing the command line.""" + print(f" $ {' '.join(args)}") + r = subprocess.run(args, cwd=cwd, capture_output=True, text=True) + if r.stdout.strip(): + print(" " + r.stdout.strip().replace("\n", "\n ")) + if check and r.returncode != 0: + print(" " + (r.stderr.strip() or "failed").replace("\n", "\n ")) + sys.exit(f"command failed: {' '.join(args)}") + return r.stdout + + +def audit(path: Path) -> dict[str, str]: + """Deterministic audit (no LLM): every control resolves to PASS or FAIL.""" + import darnit_csl # noqa: F401 - registers the plugin handlers + + from darnit.config import load_controls_from_effective, load_effective_config_by_name + from darnit.tools.audit import run_sieve_audit + + cfg = load_effective_config_by_name("community-spec", str(path)) + controls = load_controls_from_effective(cfg) + results, _ = run_sieve_audit( + owner="", repo="", local_path=str(path), default_branch="main", + level=3, controls=controls, apply_user_config=False, stop_on_llm=False, + ) + return {r["id"]: r["status"] for r in results} + + +def show(title: str, statuses: dict[str, str]) -> None: + ok = sum(1 for s in statuses.values() if s == "PASS") + print(f"\n{title}: {ok}/{len(statuses)} PASS") + for cid in sorted(statuses): + mark = "ok " if statuses[cid] == "PASS" else "FAIL" + print(f" {mark} {cid}") + + +def scan_placeholders(repo: Path) -> list[str]: + """Return governance files that still contain unfilled template text.""" + from darnit_csl.mcp_tools import _looks_like_placeholder + + hits = [] + for f in sorted((repo / "governance").glob("*.md")): + # 01 is the verbatim CSL license text; its legal prose is not a placeholder. + if f.name.startswith("01-"): + continue + if _looks_like_placeholder(f.read_text(encoding="utf-8")): + hits.append(f.name) + return hits + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--repo", required=True, help="owner/name, e.g. uptane/uptane-standard") + p.add_argument("--scope-file", help="markdown file holding the drafted scope") + p.add_argument("--contacts", default="", help="real Code of Conduct contacts") + p.add_argument("--coc-policy", choices=["csl", "umbrella", "org"], default="csl", + help="org = point the notices at an existing project/org Code of Conduct") + p.add_argument("--coc-reference", default="", + help="markdown sentence linking the existing CoC (required with --coc-policy org)") + p.add_argument("--code-license", default="Apache-2.0") + p.add_argument("--governance-mode", choices=["csl", "umbrella"], default="csl", + help="'umbrella' references existing project governance instead " + "of asserting the verbatim CSL policy") + p.add_argument("--governance-reference", default="", + help="what the umbrella governance is (name or URL)") + p.add_argument("--spec-name", default="") + p.add_argument("--workdir", default=".", help="where repos are cloned") + p.add_argument("--branch", default="csl-compliance") + p.add_argument("--dir", default=None, help="override local directory name") + p.add_argument("--audit-only", action="store_true", help="baseline audit, write nothing") + a = p.parse_args() + + owner, _, name = a.repo.partition("/") + if not owner or not name: + return print("--repo must be owner/name") or 1 + slug = a.dir or (f"{owner}-{name}" if name in _GENERIC else name) + workdir = Path(a.workdir).resolve() + repo_dir = workdir / slug + + print(f"== {a.repo} -> {repo_dir}") + if not repo_dir.exists(): + sh(["git", "clone", f"https://github.com/{a.repo}.git", slug], cwd=workdir) + branches = sh(["git", "branch", "--list", a.branch], cwd=repo_dir) + sh(["git", "checkout"] + ([] if branches.strip() else ["-b"]) + [a.branch], cwd=repo_dir) + + show("baseline", audit(repo_dir)) + + if a.audit_only or not a.scope_file: + tmpl = workdir / f"{slug}-scope.md" + if not tmpl.exists(): + tmpl.write_text(_SCOPE_TEMPLATE, encoding="utf-8") + print(f"\nscope template written: {tmpl}") + print("\nNext: fill in that file, then re-run with") + print(f' --scope-file "{tmpl}" --contacts "Name , Name "') + print(' (or, if the project already has a Code of Conduct: ' + '--coc-policy org --coc-reference "")') + return 0 + + scope = Path(a.scope_file).read_text(encoding="utf-8").strip() + # The csl_scope template already supplies the "# Scope" heading and the + # closing "Any changes of Scope are not retroactive." line. Strip them from + # the supplied body so they are not emitted twice. + scope = re.sub(r"\A#\s*Scope\s*\n+", "", scope) + scope = re.sub(r"\n*Any changes of Scope are not retroactive\.\s*\Z", "", scope).strip() + if not scope: + return print("scope file is empty") or 1 + + from darnit_csl.mcp_tools import remediate_community_spec + + print("\n== remediate") + report = asyncio.run(remediate_community_spec( + local_path=str(repo_dir), scope=scope, coc_contacts=a.contacts, + code_license=a.code_license, spec_name=a.spec_name or name, + governance_mode=a.governance_mode, + governance_reference=a.governance_reference, + coc_policy=a.coc_policy, coc_reference=a.coc_reference, + )) + print(" " + report.replace("\n", "\n ")) + if report.lstrip().startswith("Error:"): + return 1 + + hits = scan_placeholders(repo_dir) + if hits: + print(f"\nPLACEHOLDER TEXT STILL PRESENT: {', '.join(hits)}") + return 1 + print("\nplaceholder scan: clean") + + show("after remediation", audit(repo_dir)) + + fork = f"{owner}-{name}" if name in _GENERIC else name + print(f""" +== review these before committing + {repo_dir / 'governance' / '02-scope.md'} + {repo_dir / 'governance' / '03-notices.md'} + {repo_dir / 'governance' / '05-governance.md'} + +== then + cd {repo_dir} + git add governance README.md README.rst + git commit -m "Add Community Specification License (CSL 1.0) file set" + gh repo fork {a.repo} --clone=false --fork-name {fork} + git remote rename origin upstream + git remote add origin https://github.com//{fork}.git + git push -u origin {a.branch} + gh pr create --repo {a.repo} --draft --base $(git symbolic-ref --short refs/remotes/upstream/HEAD 2>/dev/null | sed 's|upstream/||' || echo main) \\ + --title "Add Community Specification License (CSL 1.0) files" +""") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/darnit/remediation/test_template_file_resolution.py b/tests/darnit/remediation/test_template_file_resolution.py index 245dc580..2c4ed64f 100644 --- a/tests/darnit/remediation/test_template_file_resolution.py +++ b/tests/darnit/remediation/test_template_file_resolution.py @@ -143,3 +143,17 @@ def test_oserror_at_read_time_logs_warning(self, tmp_path, executor_factory, cap assert content is None assert "Failed to read template file" in caplog.text assert "Simulated race condition" in caplog.text + +def test_non_ascii_template_read_as_utf8(tmp_path, executor_factory): + body = "# Title\n\nWorking Group\u2019s \u201cScope\u201d \u2014 not retroactive.\n" + pkg_dir = tmp_path / "pkg" + pkg_dir.mkdir() + tmpl_dir = pkg_dir / "templates" + tmpl_dir.mkdir() + (tmpl_dir / "legal.tmpl").write_text(body, encoding="utf-8") + executor = executor_factory( + local_path_str=str(tmp_path / "repo"), + templates={"legal": TemplateConfig(file="templates/legal.tmpl")}, + framework_path_str=str(pkg_dir / "framework.toml"), + ) + assert executor._get_template_content("legal") == body diff --git a/tests/darnit/server/test_registry.py b/tests/darnit/server/test_registry.py index 08698980..01ff7922 100644 --- a/tests/darnit/server/test_registry.py +++ b/tests/darnit/server/test_registry.py @@ -191,3 +191,48 @@ def test_load_handler_function_not_found(self): registry = ToolRegistry() with pytest.raises(AttributeError): registry.load_handler(spec) + + +class TestBuiltinSignatureStripping: + """The bound builtin must not expose the injected _framework_name. + + functools.wraps makes inspect.signature follow __wrapped__ back to the + base function, which still declares _framework_name. FastMCP rejects + tool parameters starting with '_', so registry._load_builtin strips it + from the exposed signature. + """ + + def test_framework_name_absent_from_exposed_signature(self): + import inspect + + from darnit.server.tools import BUILTIN_TOOLS + + assert "_framework_name" in inspect.signature(BUILTIN_TOOLS["audit"]).parameters + + registry = ToolRegistry() + spec = ToolSpec( + name="audit", + handler="", + description="Audit tool", + builtin="audit", + ) + bound = registry._load_builtin(spec, "openssf-baseline") + + params = inspect.signature(bound).parameters + assert "_framework_name" not in params + assert not any(name.startswith("_") for name in params) + + def test_other_parameters_are_preserved(self): + import inspect + + from darnit.server.tools import BUILTIN_TOOLS + + base_params = [ + name for name in inspect.signature(BUILTIN_TOOLS["audit"]).parameters if name != "_framework_name" + ] + + registry = ToolRegistry() + spec = ToolSpec(name="audit", handler="", description="Audit tool", builtin="audit") + bound = registry._load_builtin(spec, "openssf-baseline") + + assert list(inspect.signature(bound).parameters) == base_params diff --git a/tests/darnit_csl/__init__.py b/tests/darnit_csl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/darnit_csl/test_csl.py b/tests/darnit_csl/test_csl.py new file mode 100644 index 00000000..3569fb7f --- /dev/null +++ b/tests/darnit_csl/test_csl.py @@ -0,0 +1,434 @@ +"""Tests for the darnit-csl (Community Specification License) plugin.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from darnit_csl import get_framework_path, register +from darnit_csl.implementation import CommunitySpecImplementation + +pytestmark = pytest.mark.unit + +# --------------------------------------------------------------------------- +# Plugin protocol + TOML validity +# --------------------------------------------------------------------------- + + +class TestImplementation: + def test_register_returns_instance(self) -> None: + impl = register() + assert isinstance(impl, CommunitySpecImplementation) + + def test_identity(self) -> None: + impl = CommunitySpecImplementation() + assert impl.name == "community-spec" + assert impl.spec_version == "CSL 1.0" + + def test_framework_path_exists(self) -> None: + path = get_framework_path() + assert path.is_file() + assert path.name == "community-spec.toml" + + def test_framework_config_loads_and_validates(self) -> None: + from darnit.config import load_framework_config, validate_framework_config + + cfg = load_framework_config(get_framework_path()) + assert cfg.metadata.name == "community-spec" + # 7 controls, all templates resolvable + assert len(cfg.controls) == 7 + assert validate_framework_config(cfg) == [] + + def test_templates_resolve_on_disk(self) -> None: + base = get_framework_path().parent + for name in ( + "csl_cla", + "csl_license", + "csl_scope", + "csl_notices", + "csl_license_dual", + "csl_governance", + "csl_governance_reference", + ): + assert (base / "templates" / f"{name}.tmpl").is_file() + + +# --------------------------------------------------------------------------- +# Content-validity audit: the novel logic (missing / placeholder / filled) +# --------------------------------------------------------------------------- + + +def _status(cid: str, repo: Path, framework_name: str = "community-spec") -> str: + """Run a single control through the canonical audit pipeline; return its status.""" + from darnit.tools.audit import run_sieve_audit + + results, _ = run_sieve_audit( + owner="o", + repo="r", + local_path=str(repo), + default_branch="main", + apply_user_config=False, + framework_name=framework_name, + stop_on_llm=False, + ) + match = [r for r in results if r.get("id") == cid] + assert match, f"no result for {cid}" + return match[0].get("status", "UNKNOWN") + + +SCOPE_PLACEHOLDER = ( + "# Scope\n\n[Include a detailed description of this Working Group's Scope.]\n\n" + "Any changes of Scope are not retroactive.\n" +) +SCOPE_FILLED = ( + "# Scope\n\nThis Working Group standardizes the Foo format.\n\n" + "Any changes of Scope are not retroactive.\n" +) +NOTICES_BLANK = ( + "# Notices\n\n## Code of Conduct\n\n" + "Contact for Code of Conduct issues or inquiries: _________________\n" +) +NOTICES_FILLED = ( + "# Notices\n\n## Code of Conduct\n\n" + "Contact for Code of Conduct issues or inquiries: Jane (@jane), John (@john)\n" +) +NOTICES_WITH_GUIDANCE = ( + "# Notices\n\n## Code of Conduct\n\n" + "Contact for Code of Conduct issues or inquiries: Jane (@jane), John (@john)\n\n" + "[Ideally list two different individuals above (not a generic mailing list) " + "as someone submitting a Code of Conduct complaint will want to know exactly " + "who is receiving the complaint. We recommend two individuals in the case one " + "of the individuals is the subject of or directly involved in the subject of a complaint.]\n" +) + + +class TestScopeContentAudit: + """CSL-02.01 must distinguish missing / placeholder / filled.""" + + def test_missing_fails(self, tmp_path: Path) -> None: + assert _status("CSL-02.01", tmp_path) == "FAIL" + + def test_placeholder_fails(self, tmp_path: Path) -> None: + gov = tmp_path / "governance" + gov.mkdir() + (gov / "02-scope.md").write_text(SCOPE_PLACEHOLDER) + assert _status("CSL-02.01", tmp_path) == "FAIL" + + def test_filled_passes(self, tmp_path: Path) -> None: + gov = tmp_path / "governance" + gov.mkdir() + (gov / "02-scope.md").write_text(SCOPE_FILLED) + assert _status("CSL-02.01", tmp_path) == "PASS" + + +class TestNoticesContentAudit: + """CSL-03.01 must fail when the CoC contact line is still blank.""" + + def test_blank_contact_fails(self, tmp_path: Path) -> None: + gov = tmp_path / "governance" + gov.mkdir() + (gov / "03-notices.md").write_text(NOTICES_BLANK) + assert _status("CSL-03.01", tmp_path) == "FAIL" + + def test_filled_contact_passes(self, tmp_path: Path) -> None: + gov = tmp_path / "governance" + gov.mkdir() + (gov / "03-notices.md").write_text(NOTICES_FILLED) + assert _status("CSL-03.01", tmp_path) == "PASS" + + def test_leftover_guidance_text_fails(self, tmp_path: Path) -> None: + # Real contacts but the bracketed drafting guidance was never removed. + gov = tmp_path / "governance" + gov.mkdir() + (gov / "03-notices.md").write_text(NOTICES_WITH_GUIDANCE) + assert _status("CSL-03.01", tmp_path) == "FAIL" + + +CSL_LICENSE_TEXT = ( + "# Community Specification License 1.0\n\n" + "**The Purpose of this License.** This License sets forth the terms ...\n" +) +APACHE_LICENSE_TEXT = " Apache License\n Version 2.0\n" + + +class TestLicenseContentDetection: + """CSL-01.02 must detect the CSL license by content, under any filename + (TUF ships it as LICENSE.md, Uptane as LICENSE), and must NOT match a + non-CSL license.""" + + def test_csl_license_named_license_md_passes(self, tmp_path: Path) -> None: + (tmp_path / "LICENSE.md").write_text(CSL_LICENSE_TEXT) + assert _status("CSL-01.02", tmp_path) == "PASS" + + def test_csl_license_named_license_no_ext_passes(self, tmp_path: Path) -> None: + (tmp_path / "LICENSE").write_text(CSL_LICENSE_TEXT) + assert _status("CSL-01.02", tmp_path) == "PASS" + + def test_apache_license_fails(self, tmp_path: Path) -> None: + (tmp_path / "LICENSE").write_text(APACHE_LICENSE_TEXT) + assert _status("CSL-01.02", tmp_path) == "FAIL" + + def test_no_license_fails(self, tmp_path: Path) -> None: + assert _status("CSL-01.02", tmp_path) == "FAIL" + + def test_dual_license_reference_does_not_false_match(self, tmp_path: Path) -> None: + # 04-license.md references the CSL by name in prose but is not the license. + gov = tmp_path / "governance" + gov.mkdir() + (gov / "04-license.md").write_text( + "# Licenses\n\nSpecifications are subject to the Community Specification License 1.0.\n" + ) + assert _status("CSL-01.02", tmp_path) == "FAIL" + + +class TestClaContentDetection: + """CSL-01.01 must detect the CLA by content under a non-canonical name.""" + + def test_cla_named_cla_md_passes(self, tmp_path: Path) -> None: + (tmp_path / "CLA.md").write_text( + "# Community Specification Contributor License Agreement 1.0\n\nBy making a Contribution ...\n" + ) + assert _status("CSL-01.01", tmp_path) == "PASS" + + def test_no_cla_fails(self, tmp_path: Path) -> None: + assert _status("CSL-01.01", tmp_path) == "FAIL" + + +FULL_CSL_GOVERNANCE = ( + "# Community Specification Governance Policy 1.0\n\n" + "This document provides the governance policy for specifications ...\n" +) +UMBRELLA_GOVERNANCE = ( + "# Governance\n\nThis Working Group develops specifications using the " + "Community Specification process. The following Community Specification " + "Roles apply: Maintainer, Editor, Participants.\n" +) +GENERIC_GOVERNANCE = ( + "# Governance\n\n## Project Roles\n\nContributors submit pull requests. " + "Maintainers review and merge. Decisions are by lazy consensus.\n" +) + + +class TestGovernanceContentDetection: + """CSL-05.01 must require CSL-specific governance, not just any GOVERNANCE.md.""" + + def test_full_csl_policy_passes(self, tmp_path: Path) -> None: + (tmp_path / "GOVERNANCE.md").write_text(FULL_CSL_GOVERNANCE) + assert _status("CSL-05.01", tmp_path) == "PASS" + + def test_umbrella_reference_passes(self, tmp_path: Path) -> None: + # References an umbrella governance but documents the CSL roles/process. + (tmp_path / "GOVERNANCE.md").write_text(UMBRELLA_GOVERNANCE) + assert _status("CSL-05.01", tmp_path) == "PASS" + + def test_generic_governance_fails(self, tmp_path: Path) -> None: + # A generic GOVERNANCE.md that never mentions Community Specification. + (tmp_path / "GOVERNANCE.md").write_text(GENERIC_GOVERNANCE) + assert _status("CSL-05.01", tmp_path) == "FAIL" + + def test_no_governance_fails(self, tmp_path: Path) -> None: + assert _status("CSL-05.01", tmp_path) == "FAIL" + + +# --------------------------------------------------------------------------- +# Remediation: template substitution + governance when-clause +# --------------------------------------------------------------------------- + + +def _executor(repo: Path, context_values: dict): + from darnit.config import load_framework_config + from darnit.remediation.executor import RemediationExecutor + + cfg = load_framework_config(get_framework_path()) + ex = RemediationExecutor( + local_path=str(repo), + owner="o", + repo="r", + templates=cfg.templates, + context_values=context_values, + framework_path=str(get_framework_path()), + ) + return cfg, ex + + +class TestRemediation: + def test_scope_filled_from_context(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {"csl_working_group_scope": "Standardizes Foo."}) + res = ex.execute("CSL-02.01", cfg.controls["CSL-02.01"].remediation, dry_run=False) + assert res.success + out = (tmp_path / "governance" / "02-scope.md").read_text() + assert "Standardizes Foo." in out + assert "[Include a detailed description" not in out + + def test_scope_carries_llm_enhance(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {}) + res = ex.execute("CSL-02.01", cfg.controls["CSL-02.01"].remediation, dry_run=False) + assert any("llm_enhance" in h for h in res.details.get("handlers", [])) + + def test_dual_license_uses_chosen_code_license(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {"csl_code_license": "Apache-2.0"}) + ex.execute("CSL-04.01", cfg.controls["CSL-04.01"].remediation, dry_run=False) + out = (tmp_path / "governance" / "04-license.md").read_text() + assert "Apache-2.0 license" in out + + def test_dual_license_accepts_agpl(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {"csl_code_license": "AGPL-3.0"}) + ex.execute("CSL-04.01", cfg.controls["CSL-04.01"].remediation, dry_run=False) + out = (tmp_path / "governance" / "04-license.md").read_text() + assert "AGPL-3.0 license" in out + + def test_governance_default_is_full_policy(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {}) + ex.execute("CSL-05.01", cfg.controls["CSL-05.01"].remediation, dry_run=False) + out = (tmp_path / "governance" / "05-governance.md").read_text() + assert "Community Specification Governance Policy 1.0" in out + + def test_governance_umbrella_references_existing(self, tmp_path: Path) -> None: + cfg, ex = _executor( + tmp_path, + { + "csl_governance_mode": "umbrella", + "csl_governance_reference": "CNCF governance (https://example.org/gov)", + }, + ) + ex.execute("CSL-05.01", cfg.controls["CSL-05.01"].remediation, dry_run=False) + out = (tmp_path / "governance" / "05-governance.md").read_text() + assert "CNCF governance" in out + assert "not superseded" in out + # Must be the short reference stub, not the full verbatim policy body. + assert "Ways of Working" not in out + + def test_notices_render_drops_guidance_and_fills_contacts(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {"csl_coc_contacts": "Jane (@jane), John (@john)"}) + res = ex.execute("CSL-03.01", cfg.controls["CSL-03.01"].remediation, dry_run=False) + assert res.success + out = (tmp_path / "governance" / "03-notices.md").read_text() + assert "Jane (@jane), John (@john)" in out + assert "[Ideally list two different individuals" not in out + assert "_____" not in out + + def test_cla_render_references_current_upstream_filenames(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {}) + res = ex.execute("CSL-01.01", cfg.controls["CSL-01.01"].remediation, dry_run=False) + assert res.success + out = (tmp_path / "governance" / "00-contributor-license-agreement.md").read_text() + for ref in ( + "01-community-specification-license-v1.md", + "05-governance.md", + "06-contributing.md", + "08-code-of-conduct.md", + ): + assert ref in out, ref + assert ".0_Community_Specification_License-v1.md" not in out + assert "5._Governance.md" not in out + + def test_remediated_repo_passes_reaudit(self, tmp_path: Path) -> None: + (tmp_path / "README.md").write_text( + "# Foo\n\n## Governance\n- [Scope](governance/02-scope.md)\n" + "- [Notices](governance/03-notices.md)\n" + ) + cfg, ex = _executor( + tmp_path, + { + "csl_working_group_scope": "Standardizes Foo.", + "csl_coc_contacts": "Jane (@jane), John (@john)", + }, + ) + for cid in ("CSL-01.01", "CSL-01.02", "CSL-02.01", "CSL-03.01", "CSL-04.01", "CSL-05.01"): + ex.execute(cid, cfg.controls[cid].remediation, dry_run=False) + for cid in ("CSL-01.01", "CSL-01.02", "CSL-02.01", "CSL-03.01", "CSL-04.01", "CSL-05.01", "CSL-06.01"): + assert _status(cid, tmp_path) == "PASS", cid + + +# --------------------------------------------------------------------------- +# Optional (facultative) framework: community-spec-optional +# --------------------------------------------------------------------------- + + +class TestOptionalFramework: + """The optional files (06/07/08) live in a separate framework so they never + affect the required CSL score, and are presence-checked on demand.""" + + def test_optional_framework_loads(self) -> None: + from darnit_csl import get_optional_framework_path + + from darnit.config import ( + load_framework_config, + validate_framework_config, + ) + + path = get_optional_framework_path() + assert path.is_file() + assert path.name == "community-spec-optional.toml" + cfg = load_framework_config(path) + assert cfg.metadata.name == "community-spec-optional" + assert len(cfg.controls) == 3 + assert validate_framework_config(cfg) == [] + + def test_required_framework_excludes_optional_controls(self) -> None: + # The optional IDs must not leak into the required framework. + from darnit.config import load_framework_config + + cfg = load_framework_config(get_framework_path()) + assert not any(cid.startswith("CSL-OPT-") for cid in cfg.controls) + + def test_contributing_detected(self, tmp_path: Path) -> None: + (tmp_path / "CONTRIBUTING.md").write_text("# Contributing\n") + assert _status("CSL-OPT-01", tmp_path, "community-spec-optional") == "PASS" + + def test_code_of_conduct_detected(self, tmp_path: Path) -> None: + (tmp_path / "CODE_OF_CONDUCT.md").write_text("# Code of Conduct\n") + assert _status("CSL-OPT-03", tmp_path, "community-spec-optional") == "PASS" + + def test_missing_optional_files_fail(self, tmp_path: Path) -> None: + for cid in ("CSL-OPT-01", "CSL-OPT-02", "CSL-OPT-03"): + assert _status(cid, tmp_path, "community-spec-optional") == "FAIL" + + +class TestOrgCocMode: + ORG_REF = ( + "Example is a [CNCF](https://www.cncf.io/) project and follows the " + "[Example Code of Conduct](https://github.com/example/community/blob/main/CODE-OF-CONDUCT.md)." + ) + + def test_org_render_has_reference_and_no_contact_line(self, tmp_path: Path) -> None: + cfg, ex = _executor(tmp_path, {"csl_coc_policy": "org", "csl_coc_reference": self.ORG_REF}) + res = ex.execute("CSL-03.01", cfg.controls["CSL-03.01"].remediation, dry_run=False) + assert res.success + out = (tmp_path / "governance" / "03-notices.md").read_text() + assert self.ORG_REF in out + assert "Contact for Code of Conduct issues or inquiries" not in out + assert "_____" not in out + assert "[Ideally list two different individuals" not in out + + def test_org_reference_only_notices_passes_audit(self, tmp_path: Path) -> None: + gov = tmp_path / "governance" + gov.mkdir() + (gov / "03-notices.md").write_text( + "# Notices\n\n## Code of Conduct\n\n" + self.ORG_REF + "\n\n## License Acceptance\n" + ) + assert _status("CSL-03.01", tmp_path) == "PASS" + + def test_tool_rejects_individuals_when_repo_has_coc(self, tmp_path: Path) -> None: + import asyncio + + from darnit_csl.mcp_tools import remediate_community_spec + + (tmp_path / "CODE_OF_CONDUCT.md").write_text("# CoC\n") + out = asyncio.run(remediate_community_spec( + local_path=str(tmp_path), scope="s", + coc_contacts="Jane (@jane), John (@john)", + )) + assert out.startswith("Error:") + assert "coc_policy='org'" in out + + def test_tool_org_requires_linked_reference(self, tmp_path: Path) -> None: + import asyncio + + from darnit_csl.mcp_tools import remediate_community_spec + + out = asyncio.run(remediate_community_spec( + local_path=str(tmp_path), scope="s", coc_policy="org", coc_reference="TODO", + )) + assert out.startswith("Error:") + diff --git a/uv.lock b/uv.lock index 5d8bfcf1..4319ee8a 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,7 @@ resolution-markers = [ members = [ "darnit-baseline", "darnit-core", + "darnit-csl", "darnit-example", "darnit-gittuf", "darnit-hello", @@ -522,6 +523,17 @@ requires-dist = [ ] provides-extras = ["attestation"] +[[package]] +name = "darnit-csl" +version = "0.1.0" +source = { editable = "packages/darnit-csl" } +dependencies = [ + { name = "darnit-core" }, +] + +[package.metadata] +requires-dist = [{ name = "darnit-core", editable = "packages/darnit" }] + [[package]] name = "darnit-example" version = "0.1.0" @@ -584,6 +596,7 @@ parity-tier2 = [ [package.dev-dependencies] dev = [ + { name = "darnit-csl" }, { name = "darnit-example" }, { name = "darnit-hello" }, { name = "darnit-testchecks" }, @@ -614,6 +627,7 @@ provides-extras = ["attestation", "dev", "parity-tier2"] [package.metadata.requires-dev] dev = [ + { name = "darnit-csl", editable = "packages/darnit-csl" }, { name = "darnit-example", editable = "packages/darnit-example" }, { name = "darnit-hello", editable = "packages/darnit-hello" }, { name = "darnit-testchecks", editable = "packages/darnit-testchecks" },