Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Capture the declared tool version and a best-effort, LF-normalized SHA-256 of the explicit runtime Python-source manifest in new scan JSON, HTML, SARIF, history snapshots and privacy-minimized feedback; compare/diff retains both identities. Unknown source never becomes a partial digest or blocks linting; legacy reports are not relabelled. This identifies installed source, not executing bytecode, private inputs, accuracy or reproducibility by itself. Frozen research evidence and published v0.23.0 artifacts remain unchanged.
- Optional Simplified/Traditional Chinese repository overviews, core user guides and limitation summaries, with reciprocal language links and an explicit source-revision/coverage note. Advanced references remain English; CLI messages, JSON fields, rule semantics and the published v0.23.0 distributions are unchanged.
- An actual unscored Next.js source probe replayed on Linux with the frozen 0.22.0 code, complete Git-object/source verification and separate native/root scopes. Earlier failures, the interrupted new transfer and Windows path-case mismatch remain recorded; no correctness metrics, human labels or 0.23.0-case validation are implied.
- A readable 28-line human preannotation page with unchanged source bytes, license/provenance links, input limitations and a plain-language answer format. Consent and preparation are not completed annotations; the parent corpus remains unresolved and the page itself does not change product rules.
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ read the result, and troubleshoot a failing gate.
- [Repository doctor](doctor.md)
- [Optional PR comments](pr-comments.md)
- [Score-history workflow](score-history.md) and [local history command](score-history-cli.md)
- [Report tool identity and source fingerprints](report-identity.md) (unreleased)
- [Policy and suppression schema](../schema/agentconfigscore.schema.json)

## Understanding findings
Expand Down
6 changes: 6 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ a task, or whether an instruction file is generally “good.”
**An A 100 result means that no active deterministic rule matched. It is not a
semantic quality certification.**

Unreleased source builds include [tool source identity](report-identity.md) to
distinguish implementations that declare the same version. A matching digest is
not an accuracy result or a reproducibility guarantee: repository inputs, policy,
runtime and filesystem behavior are outside that fingerprint. Missing source is
explicitly unknown, and historic reports are not relabelled.

## Instruction-file filesystem boundary

Selected instruction files and `.agentconfigscoreignore` must resolve to regular
Expand Down
67 changes: 67 additions & 0 deletions docs/report-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Report tool identity

This is an **unreleased source feature** after v0.23.0. The existing v0.23.0
PyPI packages, release tag and rolling Action ref have not been replaced.

Package versions alone do not distinguish unreleased checkouts or local patches.
New scans capture a `tool` object at the start of `analyze()`, with:

- `name`: `AgentConfigScore`.
- `version`: the package's declared version, not a claim that this is an official release.
- `source_fingerprint.format`: `acs-python-source-v1`.
- `source_fingerprint.sha256`: 64 lowercase hex characters, or `null` if any
covered source is missing, unreadable or not valid UTF-8. No partial digest is used.
- `source_fingerprint.files`: the ordered, explicit package-source coverage list.

The fingerprint reads **only the tool's own listed Python source files**. It does
not read the scanned project's contents for hashing, run Git, contact a service,
execute those source files or add a runtime dependency. A private project name,
path, instruction, credential, configuration or suppression is not a fingerprint
input. Locally patched tool source does affect the digest; review feedback before
sharing it as usual.

## Fingerprint format

For `acs-python-source-v1`, read every filename in the exported list from the
`agent_config_score` package as strict UTF-8. Normalize CRLF and lone CR to LF.
Construct a list of `[filename, normalized_text]` pairs in the exported order.
Serialize with Python `json.dumps(pairs, ensure_ascii=False, separators=(",", ":"))`,
encode as UTF-8, and SHA-256 those bytes. Whitespace and comments otherwise remain
significant. LF and CRLF checkouts with identical text therefore have the same
digest. A test requires the manifest to cover every top-level runtime `.py` module;
new source formats or normalization rules require a new format identifier.

## Where it appears

Scan JSON exposes `tool`; compare/diff JSON exposes it on both `base` and `head`.
SARIF preserves it at `runs[].tool.driver.properties.toolIdentity` and uses the
captured package version for `semanticVersion`. HTML displays the captured version
and source SHA-256, separately from its existing short **report** fingerprint.
New history snapshots retain `tool`. Privacy-minimized feedback includes the
captured version and tool-source digest, not a hash of private instructions.

Exporting an existing `Report` does not recapture its identity. Legacy/manual
reports without captured metadata export `tool: null`; their HTML/feedback says
unknown/unavailable, and SARIF omits `semanticVersion`. Existing history entries
without `tool` remain readable and are not backfilled. Existing fields and score
calculation are unchanged; JSON consumers should accept additive fields.

## Limits

This is **source identification, not a signature, security attestation or accuracy
measurement**. It hashes installed source on disk, not necessarily bytecode already
loaded in a long-lived Python process. Restart after modifying an installation;
keep both installation and repository stable during a scan. Reads are not atomic.
The manifest excludes Python itself, dependencies/build tools, package metadata,
workflows, Git revision, environment, and repository inputs/policy. A digest may be
unavailable in bytecode-only or restricted installations; scanning still proceeds
and retains the declared version.

Identical source digests do not guarantee identical results: inputs, configuration,
suppressions, Python version, OS/filesystem behavior and baseline must also match.
History summaries still compute numeric score trends across entries; changes in
tool identity or missing metadata mean a trend cannot be attributed solely to
repository improvements. Keep the raw reports and exact input/policy revisions
when investigating a difference. Frozen research protocols and their older,
separately scoped source fingerprints are not retroactively rewritten or equated
with this format.
2 changes: 2 additions & 0 deletions docs/simplified/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

AgentConfigScore 的分数衡量已经检测到的、由规则定义的风险,不衡量模型智力、任务成功率或指令总体质量。**A 100 表示没有命中生效的确定性规则,不是语义质量或安全认证。**

尚未发布的源码版本提供[工具源码身份(英文)](../report-identity.md),用于区分声明同一版本号的不同实现。相同指纹不证明准确率或可复现性:仓库输入、策略、运行环境和文件系统行为不在该指纹范围内。工具源码缺失时明确记为未知,历史报告不补标成当前版本。

## 指令文件的文件系统边界

选中的指令文件和 `.agentconfigscoreignore` 必须解析为扫描根目录内的普通文件。仓库内的文件符号链接仍受支持;发现位置、Markdown 相对链接和指令作用域使用链接所在的位置,不是共享目标的目录。用户明确指定的根目录别名可用,并以其解析后的目录为边界。
Expand Down
2 changes: 2 additions & 0 deletions docs/simplified/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ agent-config-score diff --markdown .agent-config-score/regression.md

不要把报告放在会被当成指令文件扫描的位置。JSON、HTML、SARIF 和回归报告可能包含仓库路径及发现信息,分享前先检查。输出路径不要选成源码或策略文件,避免覆盖。多个报告写入不是事务:后一个写入失败时,前一个报告可能已生成。

尚未发布的源码版本还会在 JSON、HTML、SARIF 和新的历史快照中保留声明版本及尽力读取的工具源码 SHA-256;`compare` / `diff` JSON 分别保留两次扫描的身份。它用于区分同版本号的不同实现,本身不证明准确率或可复现性。工具源码缺失时指纹不可用,但不使扫描失效。覆盖范围、兼容性和环境/输入限制见[报告身份说明(英文)](../report-identity.md);已发布的 v0.23.0 不变。

需要隐私最小化草稿时:

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/traditional/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

AgentConfigScore 的分數衡量已偵測到、由規則定義的風險,不衡量模型智力、任務成功率或指令整體品質。**A 100 表示沒有命中生效的確定性規則,不是語意品質或安全認證。**

尚未發布的原始碼版本提供[工具原始碼身分(英文)](../report-identity.md),用來區分宣告同一版本號的不同實作。相同指紋不證明準確率或可重現性:儲存庫輸入、策略、執行環境與檔案系統行為不在該指紋範圍內。工具原始碼缺失時明確記為未知,歷史報告不補標為目前版本。

## 指令檔的檔案系統邊界

選取的指令檔與 `.agentconfigscoreignore` 必須解析為掃描根目錄內的一般檔案。儲存庫內的檔案符號連結仍受支援;發現位置、Markdown 相對連結與指令作用域使用連結所在位置,不是共享目標的目錄。使用者明確指定的根目錄別名可用,並以其解析後的目錄為邊界。
Expand Down
2 changes: 2 additions & 0 deletions docs/traditional/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ agent-config-score diff --markdown .agent-config-score/regression.md

不要將報告放在會被視為指令檔掃描的位置。JSON、HTML、SARIF 與回歸報告可能包含儲存庫路徑和檢查資訊,分享前請先檢查。輸出路徑不要選擇原始碼或策略檔案,以免覆寫。多份報告寫入不是交易:後一份寫入失敗時,前一份可能已經產生。

尚未發布的原始碼版本,也會在 JSON、HTML、SARIF 與新的歷史快照中保留宣告版本及盡力讀取的工具原始碼 SHA-256;`compare` / `diff` JSON 分別保留兩次掃描的身分。它用來區分同版本號的不同實作,本身不證明準確率或可重現性。工具原始碼缺失時指紋不可用,但不使掃描失效。涵蓋範圍、相容性及環境/輸入限制見[報告身分說明(英文)](../report-identity.md);已發布的 v0.23.0 不變。

需要盡量減少私人資訊的草稿時:

```bash
Expand Down
6 changes: 6 additions & 0 deletions docs/translations.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ instruction-source containment/readability changes built on source revision
aliases, operational failures, doctor readability checks, legacy `read-error`,
and explicit non-atomic/non-sandbox boundaries. Published artifacts are unchanged.

On 2026-10-19, both guides and limitation summaries were reviewed with the English
report-identity changes built on source revision
`f806695337119d21dac486b2aa7a6133b945dfdb`: captured version/source metadata,
unknown-source behavior, additive compatibility and non-attestation limits. The
new detailed format reference remains English. Published artifacts are unchanged.

When an English source page changes, review both variants together and record
the new source revision here. Preserve executable examples, rule IDs and JSON
keys verbatim. Keep the score limitations and failure/exit semantics aligned;
Expand Down
7 changes: 7 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ messages; inspect them before sharing. Do not overwrite your source or policy
files by choosing their paths as report destinations. Multiple output writes
are not a transaction: an earlier artifact may remain if a later write fails.

Unreleased source builds also retain the declared tool version and a best-effort
tool-source SHA-256 in JSON, HTML, SARIF and new history snapshots. Compare/diff
JSON retains both scan identities. This distinguishes local implementations with
the same version, not accuracy or reproducibility by itself. Missing source makes
the digest unavailable, not the scan invalid. See [report identity](report-identity.md)
for coverage, compatibility and environment/input limits; released v0.23.0 is unchanged.

For a privacy-minimized draft instead:

```bash
Expand Down
12 changes: 12 additions & 0 deletions scripts/verify_installed_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -102,19 +103,30 @@ def verify(python: Path, expected_version: str) -> list[str]:
"--badge", str(badge), "--sarif", str(sarif)], repo))
require(scan["score"] == 100 and scan["findings"] == [], "Clean fixture is not clean")
require(scan["files"] == ["AGENTS.md"], "Unexpected discovery scope")
identity = scan["tool"]
require(identity["version"] == expected_version, "Scan tool version mismatch")
source_digest = identity["source_fingerprint"]["sha256"]
require(isinstance(source_digest, str) and re.fullmatch(r"[0-9a-f]{64}", source_digest) is not None,
"Installed wheel has no complete tool source fingerprint")
require("AgentConfigScore" in html.read_text(encoding="utf-8"), "Missing HTML report")
require("<svg" in badge.read_text(encoding="utf-8"), "Missing SVG badge")
sarif_data = json.loads(sarif.read_text(encoding="utf-8"))
require(sarif_data["version"] == "2.1.0" and sarif_data["runs"][0]["results"] == [],
"Invalid clean SARIF output")
require(sarif_data["runs"][0]["tool"]["driver"]["properties"]["toolIdentity"] == identity,
"SARIF lost scan-time identity")
require(source_digest in html.read_text(encoding="utf-8"), "HTML lost scan-time identity")
rule = json.loads(run([*cli, "rules", "curl-pipe-shell", "--json"], repo))
require(rule["code"] == "curl-pipe-shell" and rule["severity"] == "error", "Invalid rule inspection")
require(json.loads(run([*cli, "history", "--json"], repo))["history"] == [], "Unexpected history")
feedback = artifacts / "feedback.md"
run([*cli, "feedback", "--output", str(feedback)], repo)
require(repo.name not in feedback.read_text(encoding="utf-8"), "Feedback leaked fixture repository name")
require(source_digest in feedback.read_text(encoding="utf-8"), "Feedback lost tool source identity")
unchanged = json.loads(run([*cli, "diff", "--json"], repo))
require(unchanged["delta"] == 0 and unchanged["new_findings"] == [], "Unchanged diff did not pass")
require(unchanged["base"]["tool"] == identity and unchanged["head"]["tool"] == identity,
"Diff lost scan identities")
checks.append("clean scan, HTML/SVG/SARIF, rules, history, feedback, and unchanged diff")

base, head = workspace / "base", workspace / "head"
Expand Down
6 changes: 4 additions & 2 deletions src/agent_config_score/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from collections import Counter

from . import __version__
from .scanner import Report


Expand All @@ -18,6 +17,8 @@ def feedback_markdown(report: Report) -> str:
suppressed = Counter(
item.finding.code for item in report.suppressed_findings
)
version = report.tool.version if report.tool is not None else "unknown"
source = (report.tool.source_sha256 if report.tool is not None else None) or "unavailable"

lines = [
"# AgentConfigScore real-world case",
Expand All @@ -27,7 +28,8 @@ def feedback_markdown(report: Report) -> str:
"",
"## Generated result",
"",
f"- AgentConfigScore version: `{__version__}`",
f"- AgentConfigScore version: `{version}`",
f"- Tool source SHA-256: `{source}`",
f"- Score: **{report.grade} {report.score}/100**",
f"- Supported instruction files scanned: **{len(report.files)}**",
f"- Active findings: **{len(report.findings)}**",
Expand Down
2 changes: 2 additions & 0 deletions src/agent_config_score/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
def create_snapshot(report: Any, *, commit: str | None = None) -> dict[str, Any]:
"""Create a stable read-only score history snapshot."""
findings = getattr(report, "findings", [])
tool = getattr(report, "tool", None)
return {
"tool": tool.to_dict() if tool is not None else None,
"commit": commit,
"timestamp": datetime.now(timezone.utc).isoformat(),
"score": report.score,
Expand Down
58 changes: 58 additions & 0 deletions src/agent_config_score/provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Best-effort identity of the tool's own source, never repository inputs."""
from __future__ import annotations

from dataclasses import dataclass
import hashlib
from importlib import resources
import json

from . import __version__


SOURCE_FORMAT = "acs-python-source-v1"
# Explicit coverage: no recursive reads, repository files, Git commands or imports.
# Keep sorted and include new runtime modules when they are added.
SOURCE_FILES = (
"__init__.py", "__main__.py", "cli.py", "config.py", "doctor.py",
"entrypoint.py", "feedback.py", "gitdiff.py", "history.py", "initializer.py",
"provenance.py", "regression.py", "rules.py", "sarif.py", "scanner.py",
)


@dataclass(frozen=True)
class ToolIdentity:
version: str
source_sha256: str | None
source_format: str = SOURCE_FORMAT
source_files: tuple[str, ...] = SOURCE_FILES

def to_dict(self) -> dict:
return {
"name": "AgentConfigScore",
"version": self.version,
"source_fingerprint": {
"format": self.source_format,
"sha256": self.source_sha256,
"files": list(self.source_files),
},
}


def capture_tool_identity() -> ToolIdentity:
"""Capture installed source bytes, not an attestation of executing bytecode.

Do not cache: long-lived callers may change their installation between scans.
Missing/unreadable source makes the entire digest unknown, not a partial hash.
"""
try:
package = resources.files(__package__)
sources = []
for name in SOURCE_FILES:
text = package.joinpath(name).read_text(encoding="utf-8")
sources.append([name, text.replace("\r\n", "\n").replace("\r", "\n")])
payload = json.dumps(sources, ensure_ascii=False, separators=(",", ":"))
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
except (OSError, UnicodeError):
# Do not expose installation paths or raw I/O errors in shareable reports.
digest = None
return ToolIdentity(__version__, digest, SOURCE_FORMAT, SOURCE_FILES)
16 changes: 9 additions & 7 deletions src/agent_config_score/sarif.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

from . import __version__
from .rules import get_rule
from .scanner import Finding, Report

Expand Down Expand Up @@ -58,19 +57,22 @@ def sarif_report(report: Report) -> dict:
rule_indexes = {code: index for index, code in enumerate(rule_codes)}
rules = [_rule(rules_by_code[code]) for code in rule_codes]
results = [_result(finding, rule_indexes[finding.code]) for finding in report.findings]
driver = {
"name": "AgentConfigScore",
"informationUri": "https://github.com/LE0-Lin/AgentConfigScore",
"rules": rules,
}
if report.tool is not None:
driver["semanticVersion"] = report.tool.version
driver["properties"] = {"toolIdentity": report.tool.to_dict()}

return {
"$schema": SARIF_SCHEMA,
"version": SARIF_VERSION,
"runs": [
{
"tool": {
"driver": {
"name": "AgentConfigScore",
"semanticVersion": __version__,
"informationUri": "https://github.com/LE0-Lin/AgentConfigScore",
"rules": rules,
}
"driver": driver
},
"results": results,
"properties": {
Expand Down
Loading
Loading