Repository navigation
Conversation
C0 of the decision-protocol work (CAS-9). Rev4 section 2 requires a fixed
encoding contract that rejects duplicate fields and float ambiguity; the
repository had three mutually incompatible digest schemes and no duplicate-key
detection at all.
New package domain/canonical implements profile `magi-cj/1`, a strict subset of
RFC 8785 (JCS) plus MAGI input rules:
- object keys sorted by UTF-16 code unit order, arrays keep order
- RFC 8785 minimal escaping; HTML escaping (\u003c) is not applied
- MAGI: decimal integer literals only within +/-(2^53-1); "1.0", "1e0",
"-0", NaN/Infinity are rejected
- MAGI: NFC is verified, never applied (the encoder does not rewrite what it
signs); external byte-faithful material is explicitly out of scope
- `null`, absent and empty are distinct; explicit null is preserved
- strict parsing: duplicate keys (including `"a"` vs `"\u0061"`), lone
surrogates, invalid UTF-8, raw control bytes, malformed escapes, trailing
data and unknown artifact types all fail with a typed *Error, never panic
- identity: digest = SHA-256("magi:canonical:v1:<type>\n" || canonical), and
artifact_id is always derived from the digest for a closed type whitelist
Golden vectors are generated by an independent Python reference
(testdata/verify_golden.py) and reproduced by the Go implementation; the same
script is executed from the Go test suite, so the two languages must agree on
every vector (15 positive, 14 negative).
Legacy compatibility is proven, not assumed: legacy-manifest/v1,
legacy-approval-intent/v1 and legacy-a2a-request/v1 are byte-frozen by tests
that pin the exact digests already persisted in checkpoints, approvals and A2A
submission dedup keys. C0 does not re-encode, backfill or relax them.
Verified: gofmt -l (clean), go vet ./..., go test ./..., go test -race ./...
and the MySQL suites (bootstrap + adapter: 534 pass / 0 fail / 4 real-sandbox
skips) plus scripts/test-decision-cutover.py.
Addresses the first C0 review round.
F33-01: CanonicalizeStruct marshalled first, but encoding/json is lossy --
float64(1.0) renders as `1` and invalid UTF-8 becomes U+FFFD, so inputs the
contract must reject were laundered into valid JSON. The in-memory value is now
validated recursively BEFORE marshalling: floats, invalid UTF-8, non-NFC
strings, out-of-range integers, []byte (implicit base64), custom
json.Marshaler/MarshalText implementations and unsupported kinds are rejected.
F33-02: Digest/ArtifactID accepted arbitrary []byte, so non-canonical input
(reordered keys, duplicate keys, floats, empty) could obtain a stable identity.
Canonical is now an opaque type only this package can construct, and
Digest/ArtifactID are methods on it; Adopt is the checked path for bytes read
from storage (re-parse plus byte-for-byte equality).
F33-03: parsing had no resource bound. Limits{MaxBytes, MaxDepth, MaxMembers,
MaxStringBytes} are enforced before and during parsing, with explicit errors
(payload_too_large / too_deep / too_many_members / string_too_long) and
documented C0 defaults that artifact contracts may tighten via
CanonicalizeWithLimits.
F33-04: the contract conflated C0 with artifact-layer validation. It now splits
the layers explicitly (C0: lossless parsing, numbers, Unicode, canonical form,
domain separation, identity safety, ceilings; C1/C2/C3: field whitelist,
schema_version, time fields, authorization semantics, reusing
domain/validation), freezes the call order parse -> schema -> identity ->
publish, and states that the C0 layer never claims field validation.
Test/evidence additions: raw invalid-UTF-8 and bare-control-byte negatives,
in-memory lossy-conversion counterexamples (including nested maps/slices),
Adopt rejection of non-canonical bytes, zero-value identity rejection,
resource-limit boundary tests, and Unicode NFC boundary vectors (Hangul,
precomposed, astral, U+2126 which NFC must decompose). The cross-language
Python verifier is now a hard CI step instead of a skippable test.
…uct path
Second C0 review round.
F33-05: the custom-marshaler check only looked at v.Type(), but encoding/json
calls a pointer-receiver MarshalJSON on addressable struct fields. Confirmed with
`&Parent{Child: Child{N: 1}}` where (*Child).MarshalJSON returns `{"n":999}`:
the validator approved N=1 while the marshaller produced N=999, so the identity
covered different content than the validated value. hasCustomMarshaler now checks
the pointer type (recursively through pointer kinds) as well as the value type,
which covers nested struct fields, pointer fields, interface values and slice
elements, plus pointer-receiver MarshalText.
F33-06: the struct path enforced depth but not members, map-key length or a
pre-marshal size bound, so an over-budget object was fully marshalled before the
JSON path rejected it. validateInMemory now carries an inMemoryBudget: members
are counted recursively (struct fields included, since they become object
members), map keys are length-checked, and a worst-case encoded-size budget is
accumulated before json.Marshal runs. Because the budget assumes every string
byte may escape to \u00XX it is conservative, and that trade-off is documented.
Ordering is proven without memory-peak assertions: budget_internal_test.go calls
validateInMemory directly (which CanonicalizeStruct runs before marshalling) and
asserts it rejects at the budget, including a nested-member case and a check that
the pointer-receiver type is visible to the validator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A. 基线
main=a43e71e2a0e6ec3341913e7c80b200d611fc447b(开始前已复核,远端未变、无未合并 PR)codex/c0-canonical-encodingcanonicalDigest/ApprovalIntentDigest/ A2ARequestHash/ArtifactRemap的语义;不启用任何受保护外发;不启动确认性统计实验。B. 实施
现有仓库有三套互不兼容且都已持久化的摘要方案(
legacy-manifest/v1、legacy-approval-intent/v1、legacy-a2a-request/v1),且完全没有重复键检测。本 PR 新增独立、版本化的新协议 profile,而不是改变它们:backend/domain/canonical/(新增)实现magi-cj/1:1.0、1e0、-0、NaN/Infinity。null/ 字段缺失 / 空集合三态区分,显式null保留。*canonical.Error,不 panic、不静默覆盖):重复键(含"a"与"\u0061"冲突)、孤立代理项、非法 UTF-8、字符串内裸控制字节、非法转义、尾随数据、未知工件类型。digest = SHA-256("magi:canonical:v1:<type>\n" || canonical_bytes);artifact_id = <prefix>_<digest>由摘要派生;类型走封闭白名单,禁止分隔符。C. RED → GREEN
RED(真实违规,已在改动前复现) ——
encoding/json基线行为:新能力缺失时
go test ./domain/canonical/直接构建失败(no non-test Go files),即 RED。GREEN:
TestCanonicalGoldenVectors(15 个正例:字节 + 摘要逐字节一致)TestCanonicalRejectsInvalidInput(14 个反例:重复键、1.0/1e0/-0/NaN/Infinity、越界整数、非 NFC、孤立代理项、非法 JSON、尾随数据,均要求精确 error kind)TestDigestDomainSeparation、TestArtifactIDDerivedFromDigest、TestCanonicalizeIsIdempotent、TestCanonicalizeStructMatchesJSONPath、TestExplicitNullDiffersFromAbsent跨语言独立实现:
testdata/verify_golden.py是与 Go 无关的第二实现(不同语言、不同解析路径),负责生成并复核同一份 Golden Vector;Go 侧TestGoldenVectorsReproduceInPython会执行它,两语言必须对每条向量一致。遗留字节冻结(兼容性证据,非推测):
domain/execution:TestLegacyManifestDigestsAreByteFrozen固定ApprovalIntentDigest、ToolIdempotencyKey、ModelReferenceDigest、Model/Toolset/Config/ManifestDigest的既有值。application/a2a:TestLegacyA2ARequestHashIsByteFrozen固定 submission dedup key。D. 验证(实际执行)
gofmt -l .go vet ./...go test ./...go test -race ./...MAGI_TEST_MYSQL_DSN=… go test -p 1 ./bootstrap/ ./adapter/(MySQL 8.4)go test -race -p 1 ./bootstrap/ -run '^TestMySQLDecisionWriter_'python3 scripts/test-decision-cutover.pypython3 testdata/verify_golden.py testdata/golden_vectors.json4 个 SKIP 均为
TestCodeRunnerAdapter_RealSandbox_*(需真实代码沙箱),未通过、仅未执行。MySQL 为本地临时容器执行,非 CI 结果;CI 结果以本 PR checks 为准。E. 放行决定
F. 未决边界(明确记录)
magi:canonical:v1:与类型白名单已冻结;AuditReceipt/SendRecord/AcceptanceManifest需在各自 Epic 声明使用同一 profile。G. 第一轮独立审阅修订(
b1354ff)审阅对象为
a9dbd66;以下为逐项处理(全部先确认反例成立再改)。CanonicalizeStruct可绕过严格约束:确认成立——json.Marshal(float64(1.0))产出1,非法 UTF-8 产出 U+FFFD。现改为先递归校验内存值再序列化:拒绝 float、非法 UTF-8、非 NFC、越界整数、[]byte(隐式 base64)、自定义json.Marshaler/MarshalText与不支持类型。RED→GREEN:TestCanonicalizeStructRejectsLossyInput(含嵌套 map/slice)。Digest/ArtifactID接受任意字节:确认成立。现引入不透明Canonical类型(字段不导出,外部无法伪造),Digest/ArtifactID改为其方法;新增Adopt作为存储回读路径(重新解析 + 逐字节相等),乱序键/重复键/浮点/空载荷一律拒绝。RED→GREEN:TestAdoptRejectsNonCanonicalBytes、TestZeroCanonicalHasNoIdentity、TestEquivalentJSONHasSameIdentity。Limits{MaxBytes,MaxDepth,MaxMembers,MaxStringBytes},在解析前/中强制,错误为payload_too_large/too_deep/too_many_members/string_too_long;默认值为 C0 实现默认(非领域门槛),工件契约可用CanonicalizeWithLimits收紧。RED→GREEN:TestLimitsAreEnforced(含边界值恰好通过)。schema_version、时间字段、授权语义,复用domain/validation),并冻结调用顺序解析 → Schema → 身份 → 发布(§5.3);明确 C0 不宣称字段校验。测试补充:原始非法 UTF-8 与裸控制字节独立负例(
TestCanonicalRejectsRawByteViolations)、内存有损转换反例、非规范字节Adopt拒绝、零值身份拒绝、资源上限边界、Unicode 边界向量(Hangul/预组合/astral/U+2126 必须被 NFC 分解)。Golden Vector 现为 18 正例 + 15 反例,Go 与 Python 双实现一致。跨语言验证升级为 CI 硬门禁:
.github/workflows/ci.yml的 backend job 新增步骤Canonical encoding cross-language vectors,直接运行 Python 参考实现,失败即失败;Go 单测在无 python3 时仍可跳过,但 CI 不允许跳过。Unicode 数据版本政策:契约 §6 明确 NFC 接受集合由
x/text与 CPython 的 Unicode 数据共同决定,冻结向量集是两者的兼容契约;库升级引起的分歧必须通过显式 profile 决议解决,而不是接受不同字符串集合。(审阅过程中我自己的一个边界向量假设错误:前导组合符
\u0301a其实 是 NFC 稳定的,已被跨语言校验器抓出并删除——这正是该门禁的价值。)验证:
gofmt -l(无输出)、go vet ./...、go test ./...、go test -race ./...、MySQL 8.4 套件 534 PASS / 0 FAIL / 4 SKIP、scripts/test-decision-cutover.pyOK、Python 参考实现 OK。H. 第二轮独立审阅修订(
6252d60)审阅对象为
b1354ff;两项新阻塞项均已先实测确认再修。MarshalJSON绕过校验:确认成立——实测CanonicalizeStruct(&ptrMarshalParent{Child: ptrMarshalChild{N: 1}})返回err=<nil>,产出{"child":{"n":999}},即被校验的内容与被计算身份的内容不一致。修复:
hasCustomMarshaler现同时检查指针类型(递归穿透指针种类)与值类型;覆盖嵌套结构体字段、指针字段、接口值、切片元素,以及指针接收者的
MarshalText。RED→GREEN:
TestCanonicalizeStructRejectsPointerReceiverMarshaler(7 个子用例)+ 合法对照。修复:
validateInMemory现在携带inMemoryBudget,递归累计成员数(导出结构体字段计入,因为它们会成为对象成员)、检查 Map key 长度、并在
json.Marshal之前累计最坏情况编码大小上界。上界假设每个字符串字节都可能被转义为\u00XX,因此是保守的——该取舍已写入契约 §5.1(需要精确上限时应先自行编码再用
CanonicalizeWithLimits)。顺序证据(不使用内存峰值断言):新增
budget_internal_test.go,直接调用validateInMemory(CanonicalizeStruct在序列化前调用的正是它),断言其在预算处即返回错误——含越界成员、嵌套成员计数、以及指针接收者类型对校验器可见。
验证(
6252d60):gofmt -l(无输出)、go vet ./...、go test ./...、go test -race ./...全部通过;MySQL 8.4 套件 534 PASS / 0 FAIL / 4 SKIP(4 项仍为真实 Sandbox,未执行);跨语言 CI 门禁继续运行。
另:本轮我第一次 MySQL 复跑因忘记创建
magi_ci库而出现 71 个环境性 SKIP,已修正环境后复跑得到上述 534/0/4,特此记录以免误读。