Prepare Sandwich Hime v1 release candidate source

This commit is contained in:
2026-08-23 16:25:02 -04:00
parent c11552b87a
commit efd5b997ed
64 changed files with 5276 additions and 156 deletions
+4
View File
@@ -11,4 +11,8 @@ Output must be deterministic for identical source, compiler version, runtime ABI
The template/application author chooses the generated file's license to the extent they hold the necessary rights. A project-wide license may cover generated files because inline headers would be overwritten. Sandwich Hime adds provenance metadata, not an AGPL license identifier or a compiler copyright claim. The template/application author chooses the generated file's license to the extent they hold the necessary rights. A project-wide license may cover generated files because inline headers would be overwritten. Sandwich Hime adds provenance metadata, not an AGPL license identifier or a compiler copyright claim.
The ordered v1 marker fields, compile-time ABI assertion, and source-map form
are machine-checked against
[`contracts/generated-provenance-v1.txt`](contracts/generated-provenance-v1.txt).
[OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) is an additional permission for Cole Speelman-owned generator scaffolding copied into output. It is intended to remove licensing ambiguity without claiming that every generated file is or is not a derivative work. It does not cover third-party inputs, code copied manually from the compiler, other contributors' additions unless they grant the same permission, or the Apache-licensed runtime. [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) is an additional permission for Cole Speelman-owned generator scaffolding copied into output. It is intended to remove licensing ambiguity without claiming that every generated file is or is not a derivative work. It does not cover third-party inputs, code copied manually from the compiler, other contributors' additions unless they grant the same permission, or the Apache-licensed runtime.
+4 -1
View File
@@ -7,9 +7,12 @@ Sandwich Hime deliberately separates the development tool from application runti
| Path or material | License | | Path or material | License |
| --- | --- | | --- | --- |
| Project-authored files in the repository root, `cmd/**`, `internal/**`, `docs/**`, and `scripts/**`, except the legal texts listed below | AGPL-3.0-only | | Project-authored files in the repository root, `cmd/**`, `contracts/**`, `internal/**`, `docs/**`, and `scripts/**`, except the legal texts listed below | AGPL-3.0-only |
| Nested `sando/**` runtime module, except its verbatim license text | Apache-2.0 | | Nested `sando/**` runtime module, except its verbatim license text | Apache-2.0 |
| `LICENSE`, `sando/LICENSE`, and `DCO.txt` | Their own stated copying terms and notices | | `LICENSE`, `sando/LICENSE`, and `DCO.txt` | Their own stated copying terms and notices |
| `contracts/himesan-config-v1.schema.json` | AGPL-3.0-only |
| `contracts/himesan-operation-output-v1.schema.json` | AGPL-3.0-only |
| `contracts/himesan-version-output-v1.schema.json` | AGPL-3.0-only |
| User-authored `.sando` templates | Chosen by their author, subject to rights in their inputs | | User-authored `.sando` templates | Chosen by their author, subject to rights in their inputs |
| Generated application `.sando.go` files | Chosen by the template/application author, subject to rights in their inputs and dependencies | | Generated application `.sando.go` files | Chosen by the template/application author, subject to rights in their inputs and dependencies |
+1 -1
View File
@@ -1 +1 @@
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":89,"allowlist_sha256":"2947ef034f9bfe9bd20c00e67d0033f0ff5e62e55fc2db0952696485d9eeec40","manifest_sha256":"5005a799e84f5cbb66cc9d0ced03c5897924de1b9c28adf8aaf06b2ddb4affff"} {"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":125,"allowlist_sha256":"b320a95ca85f48941ee956fde1eda46e0d9bc664da21d13c5f65a8f623063ed5","manifest_sha256":"882a7147949ab827f11102095dbe6cbd75811336e677192969c3abb25ffb0001"}
+62 -26
View File
@@ -7,30 +7,38 @@ b696cab3cf482ff5737501371cca749369b119351383e698ced42bcdbcbfc8ae ./CLA.md
797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md 797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md
86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT 86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT
f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt
73cb1b092a40c56e522c5a0ebddf2b44f347cdb57bf6994cdb305d0e5697b55e ./GENERATED_CODE.md 8ac46ef4e559f0bb43a8acb8cf6013052e09d41aa591a25c5e6fd462179eb067 ./GENERATED_CODE.md
a4570d054f072d33b8f17b0c8b162a6ee0ca37d7df2b1aee7e4b728ab350a892 ./GOVERNANCE.md a4570d054f072d33b8f17b0c8b162a6ee0ca37d7df2b1aee7e4b728ab350a892 ./GOVERNANCE.md
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE
47d857e49f89596bac9b09fc8ca57a668a33d01e2b51508acfc92ed321cdc27f ./LICENSES.md 9957e4aea02effb4d4b8012a3cc0e2d6ee1ce8be3761555c2c3e8aee27a5ee08 ./LICENSES.md
b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md 6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
fbc4a7c118ac983a1ea49dc3a9d714f5130ace21a8af59ab9fcabd6519cb6b97 ./README.md de0d4945a8246edc8da46fbc54f18349c45b6b08069309d071cb33127aee101d ./README.md
fafa1494fa1a5a5cf3b3d371155f0448d46f1f13cd394555e06396b336bc102a ./RELEASE.md 14fbed2a6de1496d7ad3783bac1b79f3f340de72545fe0dcf39de829622d7d32 ./RELEASE.md
c0e65a8bffcba71cd42d6122be25fd4993c04c8cba8c5e69108c9f5dbaca9243 ./ROADMAP.md c0e65a8bffcba71cd42d6122be25fd4993c04c8cba8c5e69108c9f5dbaca9243 ./ROADMAP.md
17e10aaab589d40b479e374523982117d2c7ffac95306493cfa4e3171108a1a8 ./SECURITY.md 813e329cfc692c0a341c47a12e2950b0c2ee403b739ad763a204d678a27765b5 ./SECURITY.md
53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md d566b8b27777cb33209602df81d654a94072ad875440c089ac41764a349b6c17 ./SPEC.md
3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md 3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md
e2ce8cbc43e4575984839b61332207f7c90e2848fa270d4c68dbc5fc394f28e7 ./cmd/himesan-release/main.go
9dda7402f76b38d417981f9e7658880b592c282c00163a3fdbfb0ab55e1edcba ./cmd/himesan/contract_test.go
8cd8db68e1300f9b78cc7235855853cbc7aeb499a921419e23a22e4d22826fcb ./cmd/himesan/main.go 8cd8db68e1300f9b78cc7235855853cbc7aeb499a921419e23a22e4d22826fcb ./cmd/himesan/main.go
0709990e28d58d50e49219690e435338b269284efb6c2d2e2c95ed6b242580df ./cmd/himesan/main_test.go 8c53127437cd8a77676fdbcdf62f6dd9346e533665bf7d5ac32f103839a24974 ./cmd/himesan/main_test.go
de004164324a640555256a91ca21a02be163bd3d231b38f08f8539c9a77ef665 ./contracts/diagnostic-codes-v1.txt
60de7d2cb9e8626136a0d197cbf55d2eed545ebab184cca66d83d5593a45b8d7 ./contracts/generated-provenance-v1.txt
e6fb36b274bf44143497b2af454e9ee0fabe6122f935cbf97e785a003277e211 ./contracts/himesan-cli-help-v1.txt
6cad9bb55238c268b25937d02ac1708bb2302c8614556648fb6c63b4479d053b ./contracts/himesan-config-v1.schema.json
09e574ed55e3a48fa117936465d3de887686cf3f2a5c4231cf598ff30e4b1a75 ./contracts/himesan-operation-output-v1.schema.json
7679c9908266811a4336be9698982e58ffedecedf2c340b082bf5dcdef1554dd ./contracts/himesan-version-output-v1.schema.json
1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md 1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md
9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md 9e4f5a80afbffe51dda3e7f9fa5221e3ef4ca516c4b7f7ca33b61ac97ff213bb ./docs/BENCHMARKS.md
5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md 5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md
a88ae86f046779db2ee4e0e7458510d782c459ab898efb8b58decaec53f72743 ./docs/COMPATIBILITY.md 8627f689faa123507102e3c2114c368f905580ea2b0151c717860de9cfa7a4b0 ./docs/COMPATIBILITY.md
5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md d1b4ef192e02b59a9d8d0eb3b1072392e633116433fa308f6ca9bbe17b75e213 ./docs/DEVELOPMENT_SERVER.md
51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md 4d92dc88ef33d4f7d952de039f842e54a3f3bfb4dbe56debf7ddb6e6c4a428f6 ./docs/DIAGNOSTICS.md
a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md
f2622e0eba601470624e862caf717060c7b73037f13f0b7bd6e9792a49463480 ./docs/SECURITY_EVIDENCE.md 32253dbe59d9b0da40354b48c2e54feb8c38d441b41efba046125571d3dabb50 ./docs/SECURITY_EVIDENCE.md
f2423c325ebe5371af43db6b09b11ea5a4ea3d3d3c14098f9d0ccd89110ea03d ./docs/THREAT_MODEL.md e600caf17a87997de330ca787bfc5f16c1b20e07f467f8d1ad450e96e2421910 ./docs/THREAT_MODEL.md
bb2fde5ffd9736ef2a46b2931484bcec7b872353c7c20821a8fa7a32946fa97d ./docs/V1_RELEASE_PLAN.md 280e5d78f5bead40f10308b750c725d2b2aca00aaf435aa07419bbc290b37fb0 ./docs/V1_RELEASE_PLAN.md
f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod
ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go
d891b9b075617050471b2ca34de73d926aaebde4ec638a5039b0d5001d3172f4 ./internal/compiler/analysis.go d891b9b075617050471b2ca34de73d926aaebde4ec638a5039b0d5001d3172f4 ./internal/compiler/analysis.go
@@ -38,10 +46,11 @@ c123bf8c8fd847962e147041172b71fef7e852819ccd7b27d4648748e02d522f ./internal/com
6ef6a0f15a5aca1c8708cbf24218372e1fca9c6fead1a5a75d261faa69651af7 ./internal/compiler/backend.go 6ef6a0f15a5aca1c8708cbf24218372e1fca9c6fead1a5a75d261faa69651af7 ./internal/compiler/backend.go
4d309392a7739b26e41cd9d9db71b52077bb8198573634ddf793ad531acc81cf ./internal/compiler/compiler_test.go 4d309392a7739b26e41cd9d9db71b52077bb8198573634ddf793ad531acc81cf ./internal/compiler/compiler_test.go
dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go
1e0f83c3e9eea34a23e8d62f8da36bb697268531e8edd9d76a1f3752abfeb563 ./internal/compiler/contract_test.go
b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go
6a3dccd0c7caf92fe2690b350f1db6f34037609b1fff6d3e68e0e229241950e5 ./internal/compiler/discover.go 6a3dccd0c7caf92fe2690b350f1db6f34037609b1fff6d3e68e0e229241950e5 ./internal/compiler/discover.go
50a8bad11deea99faf0c7c9de1266bc086b7a1247ff9dd275ffedfc69a3be81d ./internal/compiler/e2e_test.go 13fb6843b4e43fa766f8fbb7dc7f04fa7328edf159d40c5a51cd87b1a0b2dc6c ./internal/compiler/e2e_test.go
eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go 3cf1b6c26b2551ad2e8f1e06e45adfdf8b72e6f9ce7f9baf6b73209b7d788022 ./internal/compiler/fuzz_test.go
440580fcd844b2c80b1f4dddc4d83db82974e203cca298387a190e53e6a524f3 ./internal/compiler/model.go 440580fcd844b2c80b1f4dddc4d83db82974e203cca298387a190e53e6a524f3 ./internal/compiler/model.go
01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go 01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go
1f633caa019d8de9ae5bbad27e29679fcc01b3cb9a33d7d616be07140e06f38b ./internal/compiler/parse.go 1f633caa019d8de9ae5bbad27e29679fcc01b3cb9a33d7d616be07140e06f38b ./internal/compiler/parse.go
@@ -49,11 +58,13 @@ eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/com
0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go 0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go
f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando
63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go 63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go
3b9755803bb66a08ce74a973d275234a593b6a944e7fe325de35bdbe52e62c6b ./internal/devserver/browser_evidence_test.go
eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go
99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go 99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go
53cdeb4131976a4191b43ae3537b7883c18b69be9468aaae6b2cfed86c13bed2 ./internal/devserver/contract_test.go
eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go
36c4a415ee8201064b2365647f24b3cc7497bf74fefbc30db8e3d4dadcc2997b ./internal/devserver/process.go 1449c00e9f25a0a2cd2ecda99ee5b11d6b30a48ff7ba0eb2f00572f917ac332a ./internal/devserver/process.go
72074ddb02d2eae3cabe5a79c648fe0debbae3ffaa1b0800b897259eca74f209 ./internal/devserver/process_test.go c1a487deaca6ab22b4d45b9fa7cd1918834443f258945a9472967852ee55daff ./internal/devserver/process_test.go
5286e7ba5f59114e37265ab614d9dfc14bfb3003eccc93278e819e52d12f5e7a ./internal/devserver/process_unix.go 5286e7ba5f59114e37265ab614d9dfc14bfb3003eccc93278e819e52d12f5e7a ./internal/devserver/process_unix.go
c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go
6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go 6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go
@@ -61,7 +72,7 @@ c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/dev
7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go 7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go
aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go
e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go
5c0ce1eedb08c0381dd048526e9f781a2ac8501cc1142648c43d4aa07ca62299 ./internal/devserver/supervisor_test.go 40872123cec152daad73eaaabdba465033a4de57fade62790c29a8655444e67c ./internal/devserver/supervisor_test.go
e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go
b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go
76609ee5c57b1c174076689d4953e6ec5ef914ca114c27f1ca1d6e7fcabbddcf ./internal/lsp/features.go 76609ee5c57b1c174076689d4953e6ec5ef914ca114c27f1ca1d6e7fcabbddcf ./internal/lsp/features.go
@@ -69,21 +80,46 @@ edd377bca36a04ae76620a146f986e25c954fd137bab0b0e49084dbcde216ce7 ./internal/lsp
73b2ff8d6fd8fe44abec287384f0051520fe9564a50b4764538474f336624eee ./internal/lsp/protocol_test.go 73b2ff8d6fd8fe44abec287384f0051520fe9564a50b4764538474f336624eee ./internal/lsp/protocol_test.go
2a2151a0211a5048c3bc89f9a2901a3c208609f483e99144f3837f87941efff9 ./internal/lsp/security_test.go 2a2151a0211a5048c3bc89f9a2901a3c208609f483e99144f3837f87941efff9 ./internal/lsp/security_test.go
3be838b4bb0b618f46c0eab40788a1e03121da25644dedfa7542bd0fa64571e1 ./internal/lsp/server.go 3be838b4bb0b618f46c0eab40788a1e03121da25644dedfa7542bd0fa64571e1 ./internal/lsp/server.go
c919b132373d9ab715cdaf057789a3d23f27b1c9f0eded14c06d04d2a4c86ee3 ./internal/lsp/server_test.go 99f7ccc8768bd92efe6aec9039d4a1f8ba5a2d072da50c65ab3ed887e8ad9cf5 ./internal/lsp/server_test.go
4c5ade5f649dffdd0a1b0a350c15488fc601b7eba582386f39e64412387b5c91 ./internal/lsp/workspace.go 4c5ade5f649dffdd0a1b0a350c15488fc601b7eba582386f39e64412387b5c91 ./internal/lsp/workspace.go
21ec36dd353b237cd6e6705d09b2ad8a1b7dade8ad18024976e4c6fc140294d2 ./internal/releaseartifact/artifact.go
0c482e0c0ff2659ee6c650c56a7a267239cf8149283dfe5b3e680a5e3ab47121 ./internal/releaseartifact/artifact_test.go
83dd9a9d683fc443e69449bc0ac5cdc5c764f4404718c41b3a05676f7abef6d0 ./internal/releaseartifact/evidence.go
40faefc3a8ab0a6e1e28972fc515e118989da068186307133ae83c676604d741 ./internal/releaseartifact/evidence_test.go
77e7dc75b48765bb4ce926c634b0eaaded08b4d59d0951ee814001d23aebcf4c ./internal/releaseartifact/macos.go
8e31a8c4654e8a1bc8a025b350494f509b5979b61e500cc937dcbc916ee87d39 ./internal/releaseartifact/macos_test.go
f2e1873c4beb131266d6b4b8ee27acc12b7cb1baca734cc0d9586fb14484b7b0 ./internal/releaseartifact/native.go
c07cd08a7875f851981a11a4d570a7784cf453faafe0c991aa43881cd4f2e39a ./internal/releaseartifact/native_test.go
fd1babf093a791d7e50a1faa3d4b00e42a1e487ad64eece7841a3f4d81cb4072 ./internal/testpath/tempdir.go
5a8ffb091328464b84403ea54a8911e41173881a4909a334ba2e4c44dd1c0de4 ./internal/testpath/tempdir_test.go
d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go
a1ee0ec44dc45f01a3357cf93e13a81587af7f061497c25df374e2a88231f3b7 ./internal/version/version_test.go a1ee0ec44dc45f01a3357cf93e13a81587af7f061497c25df374e2a88231f3b7 ./internal/version/version_test.go
e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE
b4a7bffe678a97209881e07989563a5085aa0ead9e1b67306087dac6b97bad70 ./sando/README.md 9b9b09b595658cf65aef8f7ace9cc955f026cdd71ece0f79671bfada8bd9c260 ./sando/README.md
acfbbc860417aeabba486ca4fea530e711c3e488bc3f5f6588db102b8a58bec2 ./sando/api_contract_test.go
7e862661f19fc9fcd694ef64e65fda898186c51836335c27c21397e8dcf09c05 ./sando/benchmark_test.go
7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go 7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go
a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go
e12e2b27396ba4447d27cb1f80ef44100f7295135c26a1bdad5e13e1312a3a5f ./sando/differential_test.go
ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod
ed2b8d8040689c608623d30085a55678ae3c9d70a8a40a42435e04bff47a31ab ./sando/testdata/public-api-v1.txt
80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go 80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go
85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go 85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go
b188917e258890e6b6e4840a6fd946fc9a77cabc2068da3764f221e4a6a5df97 ./sando/write_test.go b188917e258890e6b6e4840a6fd946fc9a77cabc2068da3764f221e4a6a5df97 ./sando/write_test.go
36265470310f8463895761bb53a2137583d395d4b19b0190d038eecce1f4ce25 ./scripts/README.md 6ea995e9d7d22e09a13065a6e340df4efa0f1860b0b42409f84835c93f29962c ./scripts/PUBLIC_SNAPSHOT.md
0bc796f71c863aa898674a26c56f055e3d81cf20629ca7b32fbae87d8841e0a8 ./scripts/check-licenses.sh 2d152851bc77f6eb5ca842a8021cde386a3cae91db819cd99cedc31b3b62518f ./scripts/README.md
03d58bea32691d6d503983ddcf979fb88091eed4cb322e74a9eeb4ee434bacec ./scripts/release-check.sh 81ab4c10a436bc9b345828a55c2fe446f297fa6cfd1debfe95e1082b6e28c969 ./scripts/check-licenses.sh
78a64c7fb3a039b15a1d08b4c0b873952852287a07f670247b081e59dbb09a30 ./scripts/verify-public-install.sh 3adba1428df83d6180d4fa3c683f0604dc7cdc0c4358f3dae7595189cc40f949 ./scripts/export-public-snapshot.sh
f0cbd86759fa729064cb1c69991db2ac291792dadb6b1e1ba83794f2e390404d ./scripts/verify.sh 47dceef111d1da4345b473ff9092df7fcdab655fa1ec660e58012f75c5135d83 ./scripts/native-gate.sh
a038a002f0c895fef38a7265bcb7df01efdda57de5c4d36d0dc7c8e377798a95 ./scripts/package-macos.sh
7d1e3606f3159e0d5d222bb5b02171ad90bb2c42b0e041042dcf8e6b7692c40a ./scripts/package-native.sh
1fddbdf81e86f4310a603cedd0f24a39d6c62d810c7e1e2e6c5f7bff1d1d4198 ./scripts/public-snapshot-lib.sh
b320a95ca85f48941ee956fde1eda46e0d9bc664da21d13c5f65a8f623063ed5 ./scripts/public-snapshot.allow
183b76febe506a8e0dfaa70ddf6269749ab2c475e6c345c8adfb2ad6e7e1019b ./scripts/release-check.sh
494cf5573cc6a891d4243ec01a98c43263de75090cb3b55c9c398e545b1ec1a8 ./scripts/sign-notarize-macos.sh
7c65325664e95df1a63262bc440a599fb799b36123a68c5cc26cfb81ddd08efa ./scripts/test-public-snapshot.sh
39a80708b1c1a64c837032c6570e77dea28b2c6e6058eea40efac63ad5827431 ./scripts/verify-consumer.sh
ddcb5602a51a515ee6649e26f27d919addb0590abdcc095c374c90f5b0c2f8a3 ./scripts/verify-public-install.sh
1673257efa50b906363ccab45f4ebcf7cb377185861a1c37221c98292c2c7cf6 ./scripts/verify-real-browser.sh
d67b73b716fa795bf166df12ad91eea74225b6c76e659d0e8b088dad221865aa ./scripts/verify.sh
+7 -7
View File
@@ -48,17 +48,17 @@ semantic-version prerelease: source syntax, generated output, the runtime API,
and CLI behavior may change before final v1, and this beta is not recommended and CLI behavior may change before final v1, and this beta is not recommended
for production deployment. for production deployment.
Linux/amd64 is the maintained execution and release target. Required release Linux/amd64 and Apple Silicon macOS/arm64 are the maintained v1 execution and
evidence runs on Linux with the supported Go lines. WSL is a useful Linux release targets. Native release evidence runs with pinned Go 1.26.7 and Go
development environment, but it does not turn native Windows into a supported 1.27.0 toolchains on both platforms; the module language directive remains Go
target. Native Windows, macOS, and other operating systems may happen to build 1.25 for consumer compatibility. WSL, native Windows, Intel macOS, and other
or work and portability reports are welcome; they are not release gates or a targets may be useful development or portability environments but are not v1
maintained compatibility promise. compatibility promises.
The evidence ledger retains the exact Beta 1 Windows and Linux observations as The evidence ledger retains the exact Beta 1 Windows and Linux observations as
historical facts. Those past results do not expand the current support policy. historical facts. Those past results do not expand the current support policy.
Maintainers remain responsible for security review, triage, fixes, and release Maintainers remain responsible for security review, triage, fixes, and release
decisions on the supported Linux target. decisions on both supported native targets.
Inside an application module, add the small runtime first: Inside an application module, add the small runtime first:
+49 -11
View File
@@ -21,11 +21,10 @@ while it is the current prerelease, but it is not recommended or supported as a
production-stable dependency. Syntax, generated output, runtime APIs, CLI production-stable dependency. Syntax, generated output, runtime APIs, CLI
behavior, and diagnostics may change in a later prerelease. behavior, and diagnostics may change in a later prerelease.
Current and future beta release gates run on Linux/amd64. WSL may be used as a Beta 1 and Beta 2 retain their historical platform evidence. The v1 RC line
Linux development environment, but native Windows, macOS, and other targets raises the maintained release gate to native Linux/amd64 and Apple Silicon
are not release blockers or maintained compatibility promises. Portability macOS/arm64. WSL, native Windows, Intel macOS, and other targets remain useful
reports remain useful input; they do not transfer security review, triage, portability input but are not v1 release blockers or maintained promises.
remediation, or release responsibility away from the maintainers.
Beta tags are signed, annotated, and immutable. Beta 1 is a source/module Beta tags are signed, annotated, and immutable. Beta 1 is a source/module
release installed through the Go toolchain; it does not promise downloadable release installed through the Go toolchain; it does not promise downloadable
@@ -37,9 +36,9 @@ final v1.
An RC means the intended v1 source, runtime, CLI, diagnostics, schemas, and An RC means the intended v1 source, runtime, CLI, diagnostics, schemas, and
generated contract are frozen except for release-blocking fixes. An RC requires generated contract are frozen except for release-blocking fixes. An RC requires
maintainer-run Linux/amd64 evidence, complete release artifacts and provenance, maintainer-run Linux/amd64 and native macOS/arm64 evidence, complete release
signed tags, clean direct/proxy installs, and every RC gate in this repository. artifacts and provenance, signed tags, clean direct/proxy installs, and every
Findings produce a new RC rather than a moved tag. RC gate in this repository. Findings produce a new RC rather than a moved tag.
### Final v1 ### Final v1
@@ -121,9 +120,48 @@ development-supervisor failure tests, and reproducible repository-owned
benchmark and security results. benchmark and security results.
Release candidates require a clean canonical checkout, reviewed changelog, Release candidates require a clean canonical checkout, reviewed changelog,
compatible vanity-import metadata, reproducible Linux/amd64 binaries, signed compatible vanity-import metadata, reproducible Linux/amd64 and Darwin/arm64
annotated tags, checksums, SBOMs, vulnerability results, and verification on binaries, signed annotated tags, checksums, SBOMs, vulnerability results, and
Linux/amd64. verification on both maintained native targets. Darwin artifacts additionally
require manual Developer ID signing, notarization, stapling, and Gatekeeper
validation outside runner authority.
Human-reviewed RC evidence stays outside the source tree. After completing the
fixed document set reported by `himesan-release verify-evidence`, the release
operator seals its exact bytes and source identity once:
```sh
go run ./cmd/himesan-release evidence-manifest \
--directory "$HIMESAN_RELEASE_EVIDENCE_DIR" \
--repository gamertan/sandwich-hime \
--version v1.0.0-rc.1 \
--commit "$(git rev-parse HEAD)" \
--tree "$(git rev-parse 'HEAD^{tree}')" \
--reviewed-by "REVIEWER" \
--reviewed-at "YYYY-MM-DDTHH:MM:SSZ"
```
The manifest is created without overwrite. Changing any document or candidate
identity requires a fresh review directory and manifest; deleting a manifest
is not an approval shortcut. `release-check.sh --public` verifies the sealed
digests and identities but never substitutes for the human review itself.
Native receipts use an equally strict, extraction-scratch-free layout. Download
runner ZIPs outside this directory, then copy only each checksummed receipt pair
into the exact four lanes:
```text
$HIMESAN_NATIVE_EVIDENCE_DIR/
├── darwin-arm64-go1.26.7/TEND-CI-VERIFICATION.json{,.sha256}
├── darwin-arm64-go1.27.0/TEND-CI-VERIFICATION.json{,.sha256}
├── linux-amd64-go1.26.7/TEND-CI-VERIFICATION.json{,.sha256}
└── linux-amd64-go1.27.0/TEND-CI-VERIFICATION.json{,.sha256}
```
ZIPs, additional files, renamed lanes, development-repository identities, and
receipts for a public commit other than current canonical `main` are rejected.
The strict layout prevents extraction debris or a nearby historical run from
being mistaken for the reviewed native receipt set.
## Public source and artifacts ## Public source and artifacts
+24 -1
View File
@@ -10,7 +10,8 @@ fixes while this pair is current. This is not production support,
an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs. an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs.
The community is invited to help find portability gaps outside the maintained The community is invited to help find portability gaps outside the maintained
Linux target, but those reports do not create a support or release commitment. Linux/amd64 and Darwin/arm64 targets, but those reports do not create a support
or release commitment.
That invitation does not outsource security assurance. Maintainers retain That invitation does not outsource security assurance. Maintainers retain
responsibility for vulnerability review, triage, remediation decisions, responsibility for vulnerability review, triage, remediation decisions,
advisories, and release decisions. advisories, and release decisions.
@@ -76,6 +77,28 @@ may be delivered through a new immutable version, a retraction, an advisory, or
documentation that narrows an incorrect guarantee. Published tags will not be documentation that narrows an incorrect guarantee. Published tags will not be
moved or silently replaced. moved or silently replaced.
## Severity, advisories, retractions, and CVEs
Triage considers attacker prerequisites, affected trust boundary, data or code
impact, availability impact, default reachability, and whether exploitation
crosses the documented application-owned capability boundary. The project uses
plain-language critical, high, moderate, and low labels; it does not publish a
CVSS score unless the vector and calculation have been reviewed.
An accepted vulnerability affecting a published version receives a canonical
advisory or release security note identifying affected and fixed versions,
mitigations, credit, and disclosure chronology as safely available. The
maintainer requests a CVE when a disclosed vulnerability materially affects a
published release and a stable public advisory exists. A CVE is an identifier,
not evidence of severity or independent validation.
Published source and tags are never deleted or moved to hide a faulty release.
When Go tooling supports the boundary, a later immutable module version may
use a `retract` directive with a concise rationale. Otherwise the project
publishes a superseding version and marks the affected release in canonical
release notes. Retraction does not erase source, evidence, or the disclosure
record.
## Scope and trust boundary ## Scope and trust boundary
The most useful reports concern: The most useful reports concern:
+52 -1
View File
@@ -21,7 +21,23 @@ func Card(card model.Card)
The v1 target is `go`. Other target names are rejected. The explicit target is an architectural seam for a possible future San backend; it is not a promise that such a backend exists. The v1 target is `go`. Other target names are rejected. The explicit target is an architectural seam for a possible future San backend; it is not a promise that such a backend exists.
The header permits one package clause, ordinary Go imports, and one bodyless, receiverless function declaration. The component name is the function name and its parameters form the generated typed API. Multiple components, methods, global declarations, and executable initialization in the header are errors. The header permits one package clause, ordinary Go imports, and one bodyless,
receiverless function declaration. The component name is the function name and
its parameters form the generated typed API. A Go type-parameter list is part
of the v1 grammar and is preserved after `go/format` normalization:
```sando
<?sando go
package views
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>
```
Constraints, inference, and instantiation use ordinary Go rules; Sandwich Hime
does not add a second generic type system. Multiple components, methods, global
declarations, and executable initialization in the header are errors.
## Template tags ## Template tags
@@ -42,6 +58,12 @@ For `func Card(card model.Card)`, generation emits:
func Card(card model.Card) sando.Component func Card(card model.Card) sando.Component
``` ```
For the generic example above, generation emits:
```go
func List[T ~string](values []T) sando.Component
```
The component captures its typed parameters and renders later with a context and writer. All static writes, escaping operations, nested component renders, and application-provided writers propagate errors. The component captures its typed parameters and renders later with a context and writer. All static writes, escaping operations, nested component renders, and application-provided writers propagate errors.
Generated files are adjacent to their source (`card.sando.go`), formatted with `go/format`, and contain the compiler version, runtime ABI, source digest, and source mappings. Hime-san does not inject the compiler's AGPL license identifier or copyright claim. An application rightsholder remains free to select AGPL intentionally through the application's own license policy. Generated files are adjacent to their source (`card.sando.go`), formatted with `go/format`, and contain the compiler version, runtime ABI, source digest, and source mappings. Hime-san does not inject the compiler's AGPL license identifier or copyright claim. An application rightsholder remains free to select AGPL intentionally through the application's own license policy.
@@ -62,6 +84,35 @@ V1 recognizes:
Ordinary URL values are attribute-escaped and rejected at render time when their normalized scheme is dangerous. Only `sando.TrustedURL`, made by an explicit `sando.TrustURL` call in trusted Go code, may bypass that scheme policy. The analogous trusted HTML, JavaScript, and CSS types are opaque and have conspicuous constructors. Ordinary URL values are attribute-escaped and rejected at render time when their normalized scheme is dangerous. Only `sando.TrustedURL`, made by an explicit `sando.TrustURL` call in trusted Go code, may bypass that scheme policy. The analogous trusted HTML, JavaScript, and CSS types are opaque and have conspicuous constructors.
### V1 output matrix
| Template position | Ordinary value | Explicit trusted value | Unsupported or rejected |
| --- | --- | --- | --- |
| HTML text | HTML-escaped by `WriteText` | `TrustedHTML` is written verbatim | Dynamic markup structure remains the caller's capability boundary |
| `title`/`textarea` RCDATA | HTML-escaped by `WriteRCDATA` | Trusted wrappers are still escaped | Closing the element through a value |
| Quoted ordinary attribute | HTML-escaped by `WriteAttr` | Trusted wrappers stringify, then escape | Unquoted values, dynamic names, and event-handler attributes |
| Quoted URL attribute | Scheme-checked, normalized, then attribute-escaped by `WriteURL` | `TrustedURL` bypasses only the scheme check | Ambiguous schemes, controls, and non-allowlisted schemes |
| `script` data | Not accepted | `TrustedJS` is written verbatim | Plain strings and ambiguous escaped-script parser states |
| `style` data | Not accepted | `TrustedCSS` is written verbatim | Plain strings and dynamic style attributes |
| Ordinary HTML content | `<?~` renders a `Component` | Handwritten components are explicit trusted-output capabilities | Component rendering in attributes, tags, comments, RCDATA, script, or style |
Ordinary values use `fmt.Sprint` semantics before contextual normalization.
Invalid UTF-8 and NUL bytes become U+FFFD in text, RCDATA, attribute, and URL
helpers. A nil render context, writer, component, typed-nil component, or
typed-nil writer produces the corresponding stable sentinel error rather than
a panic. All writer errors and short writes propagate.
Relative URLs and the `http`, `https`, `mailto`, and `tel` schemes are accepted.
Leading and trailing Unicode whitespace is removed before classification;
ASCII controls, ambiguous scheme syntax, and every other ordinary scheme are
rejected before bytes are written. `TrustedURL` does not bypass quoted-attribute
escaping.
Handwritten Go statements, handwritten components, and every `Trust*` call are
application-owned capabilities. Sandwich Hime does not sanitize or sandbox
trusted source, prevent panics or blocking inside application code, provide
HTTP routing, or infer that a string became safe elsewhere in the program.
## Compatibility ## Compatibility
V1 is a clean break from the 2025 prototype. `.go.hime`, injected `himesan` helper directories, `SandoName(io.Writer)` functions, Go plugins, and nested demonstration modules are not accepted or generated. `.san` is not and will never be a Sandwich Hime extension. V1 is a clean break from the 2025 prototype. `.go.hime`, injected `himesan` helper directories, `SandoName(io.Writer)` functions, Go plugins, and nested demonstration modules are not accepted or generated. `.san` is not and will never be a Sandwich Hime extension.
+213
View File
@@ -0,0 +1,213 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Command himesan-release creates deterministic unsigned artifacts and native
// verification receipts. Signing and notarization intentionally remain outside
// this command and outside unattended runner authority.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"gamertan.com/sandwich-hime/internal/releaseartifact"
)
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "himesan-release: %v\n", err)
os.Exit(1)
}
}
func run(arguments []string) error {
if len(arguments) == 0 {
return errors.New("usage: himesan-release <package|receipt|evidence-manifest|verify-evidence|verify-native|extract-macos|finalize-macos> [options]")
}
switch arguments[0] {
case "package":
return runPackage(arguments[1:])
case "receipt":
return runReceipt(arguments[1:])
case "evidence-manifest":
return runEvidenceManifest(arguments[1:])
case "verify-evidence":
return runVerifyEvidence(arguments[1:])
case "verify-native":
return runVerifyNative(arguments[1:])
case "extract-macos":
return runExtractMacOS(arguments[1:])
case "finalize-macos":
return runFinalizeMacOS(arguments[1:])
default:
return fmt.Errorf("unknown command %q", arguments[0])
}
}
func runVerifyNative(arguments []string) error {
flags := flag.NewFlagSet("verify-native", flag.ContinueOnError)
var directory string
var expected releaseartifact.NativeReceiptExpectation
flags.StringVar(&directory, "directory", "", "four-lane native receipt directory")
flags.StringVar(&expected.Repository, "repository", "", "repository identity")
flags.StringVar(&expected.Commit, "commit", "", "source commit")
flags.StringVar(&expected.Tree, "tree", "", "source tree")
if err := flags.Parse(arguments); err != nil {
return err
}
summary, err := releaseartifact.VerifyNativeReceiptSet(directory, expected)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(summary)
}
func runExtractMacOS(arguments []string) error {
flags := flag.NewFlagSet("extract-macos", flag.ContinueOnError)
var archive, checksum, output string
flags.StringVar(&archive, "archive", "", "unsigned Darwin/arm64 archive")
flags.StringVar(&checksum, "sha256", "", "approved archive SHA-256")
flags.StringVar(&output, "output", "", "empty extraction parent directory")
if err := flags.Parse(arguments); err != nil {
return err
}
root, err := releaseartifact.ExtractVerifiedMacOSPackage(archive, checksum, output)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]string{"root": root, "unsigned_archive_sha256": checksum})
}
func runFinalizeMacOS(arguments []string) error {
flags := flag.NewFlagSet("finalize-macos", flag.ContinueOnError)
var options releaseartifact.MacOSSigningOptions
flags.StringVar(&options.Directory, "directory", "", "extracted signed distribution directory")
flags.StringVar(&options.UnsignedArchiveSHA256, "unsigned-archive-sha256", "", "approved unsigned archive SHA-256")
flags.StringVar(&options.Identity, "identity", "", "Developer ID identity")
flags.StringVar(&options.Identifier, "identifier", "", "signed binary identifier")
flags.StringVar(&options.FinalizedAt, "finalized-at", "", "RFC3339 finalization time")
if err := flags.Parse(arguments); err != nil {
return err
}
if err := releaseartifact.FinalizeSignedMacOSDistribution(options); err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]any{"valid": true, "directory": options.Directory})
}
func runEvidenceManifest(arguments []string) error {
flags := flag.NewFlagSet("evidence-manifest", flag.ContinueOnError)
var directory string
var identity releaseartifact.EvidenceIdentity
flags.StringVar(&directory, "directory", "", "reviewed evidence directory")
bindEvidenceIdentityFlags(flags, &identity, true)
if err := flags.Parse(arguments); err != nil {
return err
}
checksum, err := releaseartifact.WriteEvidenceManifest(directory, identity)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]string{"manifest": filepath.Join(directory, "RELEASE-EVIDENCE.json"), "sha256": checksum})
}
func runVerifyEvidence(arguments []string) error {
flags := flag.NewFlagSet("verify-evidence", flag.ContinueOnError)
var directory string
var identity releaseartifact.EvidenceIdentity
flags.StringVar(&directory, "directory", "", "sealed evidence directory")
bindEvidenceIdentityFlags(flags, &identity, false)
if err := flags.Parse(arguments); err != nil {
return err
}
if err := releaseartifact.VerifyEvidenceManifest(directory, identity); err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]any{"valid": true, "files": releaseartifact.RequiredEvidenceFiles()})
}
func bindEvidenceIdentityFlags(flags *flag.FlagSet, identity *releaseartifact.EvidenceIdentity, review bool) {
flags.StringVar(&identity.Repository, "repository", "", "canonical repository identity")
flags.StringVar(&identity.Version, "version", "", "candidate semantic version")
flags.StringVar(&identity.Commit, "commit", "", "source commit")
flags.StringVar(&identity.Tree, "tree", "", "source tree")
if review {
flags.StringVar(&identity.ReviewedBy, "reviewed-by", "", "human reviewer identity")
flags.StringVar(&identity.ReviewedAt, "reviewed-at", "", "RFC3339 review time")
}
}
func runPackage(arguments []string) error {
flags := flag.NewFlagSet("package", flag.ContinueOnError)
var options releaseartifact.PackageOptions
flags.StringVar(&options.Version, "version", "", "candidate semantic version")
flags.StringVar(&options.Commit, "commit", "", "source commit")
flags.StringVar(&options.Tree, "tree", "", "source tree")
flags.StringVar(&options.GoVersion, "go-version", "", "Go toolchain identity")
flags.StringVar(&options.GOOS, "goos", "", "target operating system")
flags.StringVar(&options.GOARCH, "goarch", "", "target architecture")
flags.StringVar(&options.BinaryPath, "binary", "", "unsigned native binary")
flags.StringVar(&options.LicensePath, "license", "LICENSE", "license text")
flags.StringVar(&options.ReleaseNotes, "release-notes", "RELEASE.md", "release notes")
flags.StringVar(&options.OutputDirectory, "output", "", "output directory")
flags.Int64Var(&options.SourceDateEpoch, "source-date-epoch", 0, "fixed Unix timestamp")
if err := flags.Parse(arguments); err != nil {
return err
}
result, err := releaseartifact.Package(options)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(result)
}
func runReceipt(arguments []string) error {
flags := flag.NewFlagSet("receipt", flag.ContinueOnError)
var receipt releaseartifact.Receipt
var output, gates, generatedFiles string
flags.StringVar(&output, "output", "", "receipt output path")
flags.StringVar(&receipt.Repository, "repository", "", "repository identity")
flags.StringVar(&receipt.Commit, "commit", "", "source commit")
flags.StringVar(&receipt.Tree, "tree", "", "source tree")
flags.StringVar(&receipt.GOOS, "goos", "", "native operating system")
flags.StringVar(&receipt.GOARCH, "goarch", "", "native architecture")
flags.StringVar(&receipt.GoVersion, "go-version", "", "Go toolchain identity")
flags.StringVar(&receipt.RunnerVersion, "runner-version", "", "Gitea Runner version")
flags.StringVar(&receipt.RunnerName, "runner-name", "", "runner identity")
flags.StringVar(&receipt.UnsignedArtifactSHA, "artifact-sha256", "", "optional unsigned artifact digest")
flags.StringVar(&receipt.CompletedAt, "completed-at", "", "RFC3339 completion time")
flags.StringVar(&gates, "gates", "", "comma-separated successful gates")
flags.StringVar(&generatedFiles, "generated-files", "", "comma-separated generated output paths")
if err := flags.Parse(arguments); err != nil {
return err
}
if output == "" {
return errors.New("output is required")
}
receipt.SuccessfulGates = splitList(gates)
digest, err := releaseartifact.DigestFiles(splitList(generatedFiles))
if err != nil {
return err
}
receipt.GeneratedDigest = digest
checksum, err := releaseartifact.WriteReceipt(output, receipt)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]string{"receipt": output, "sha256": checksum})
}
func splitList(value string) []string {
var values []string
for _, item := range strings.Split(value, ",") {
item = strings.TrimSpace(item)
if item != "" {
values = append(values, item)
}
}
return values
}
+145
View File
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: AGPL-3.0-only
package main
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"gamertan.com/sandwich-hime/internal/testpath"
)
type contractSchema struct {
AdditionalProperties bool `json:"additionalProperties"`
Required []string `json:"required"`
Properties map[string]json.RawMessage `json:"properties"`
}
func TestV1CLIHelpContract(t *testing.T) {
t.Parallel()
want, err := os.ReadFile(filepath.Join("..", "..", "contracts", "himesan-cli-help-v1.txt"))
if err != nil {
t.Fatal(err)
}
want = bytes.TrimPrefix(want, []byte("# SPDX-License-Identifier: AGPL-3.0-only\n\n"))
var output bytes.Buffer
printHelp(&output)
if !bytes.Equal(output.Bytes(), want) {
t.Fatalf("CLI help contract drifted\n--- want ---\n%s--- got ---\n%s", want, output.Bytes())
}
}
func TestV1VersionJSONSchemaMatchesOutput(t *testing.T) {
t.Parallel()
schema := readContractSchema(t, "himesan-version-output-v1.schema.json")
var stdout, stderr bytes.Buffer
if code := run(context.Background(), []string{"version", "--json"}, &stdout, &stderr); code != 0 {
t.Fatalf("version exit code = %d: %s", code, stderr.String())
}
var output map[string]any
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatal(err)
}
assertObjectShape(t, output, schema, "version output")
}
func TestV1OperationJSONSchemaMatchesSuccessAndDiagnosticOutput(t *testing.T) {
t.Parallel()
schema := readContractSchema(t, "himesan-operation-output-v1.schema.json")
directory := testpath.TempDir(t)
source := filepath.Join(directory, "page.sando")
if err := os.WriteFile(source, []byte("<?sando go\npackage views\nfunc Page()\n?>\n<p>page</p>\n"), 0o600); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
if code := run(context.Background(), []string{"check", "--json", source}, &stdout, &stderr); code != 1 {
t.Fatalf("missing-output check exit code = %d, want 1: %s", code, stderr.String())
}
var output map[string]any
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
t.Fatal(err)
}
assertObjectShape(t, output, schema, "operation output")
resultSchema := nestedSchema(t, schema.Properties["result"])
result, ok := output["result"].(map[string]any)
if !ok {
t.Fatalf("result = %T, want object", output["result"])
}
assertObjectShape(t, result, resultSchema, "operation result")
files, ok := result["files"].([]any)
if !ok || len(files) != 1 {
t.Fatalf("files = %#v, want one item", result["files"])
}
filesProperty := rawObject(t, resultSchema.Properties["files"])
fileSchema := nestedSchema(t, filesProperty["items"])
assertObjectShape(t, files[0].(map[string]any), fileSchema, "file result")
diagnostics, ok := result["diagnostics"].([]any)
if !ok || len(diagnostics) == 0 {
t.Fatalf("diagnostics = %#v, want at least one item", result["diagnostics"])
}
diagnosticsProperty := rawObject(t, resultSchema.Properties["diagnostics"])
diagnosticSchema := nestedSchema(t, diagnosticsProperty["items"])
assertObjectShape(t, diagnostics[0].(map[string]any), diagnosticSchema, "diagnostic")
}
func readContractSchema(t *testing.T, name string) contractSchema {
t.Helper()
contents, err := os.ReadFile(filepath.Join("..", "..", "contracts", name))
if err != nil {
t.Fatal(err)
}
var schema contractSchema
if err := json.Unmarshal(contents, &schema); err != nil {
t.Fatalf("decode %s: %v", name, err)
}
if schema.AdditionalProperties || len(schema.Properties) == 0 {
t.Fatalf("%s is not a closed object schema", name)
}
return schema
}
func nestedSchema(t *testing.T, raw json.RawMessage) contractSchema {
t.Helper()
var schema contractSchema
if err := json.Unmarshal(raw, &schema); err != nil {
t.Fatal(err)
}
return schema
}
func rawObject(t *testing.T, raw json.RawMessage) map[string]json.RawMessage {
t.Helper()
var object map[string]json.RawMessage
if err := json.Unmarshal(raw, &object); err != nil {
t.Fatal(err)
}
return object
}
func assertObjectShape(t *testing.T, actual map[string]any, schema contractSchema, label string) {
t.Helper()
actualKeys := make([]string, 0, len(actual))
for key := range actual {
actualKeys = append(actualKeys, key)
if _, declared := schema.Properties[key]; !declared {
t.Fatalf("%s emitted undeclared property %q", label, key)
}
}
sort.Strings(actualKeys)
for _, required := range schema.Required {
if _, present := actual[required]; !present {
t.Fatalf("%s omitted required property %q (got %v)", label, required, actualKeys)
}
}
if len(actual) == 0 || reflect.ValueOf(actual).IsNil() {
t.Fatalf("%s is empty", label)
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"testing" "testing"
"gamertan.com/sandwich-hime/internal/compiler" "gamertan.com/sandwich-hime/internal/compiler"
"gamertan.com/sandwich-hime/internal/testpath"
) )
func TestRunHelpVersionAndUnknownCommand(t *testing.T) { func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
@@ -67,7 +68,7 @@ func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
func TestGenerateCheckBlessAndJSONDiagnostics(t *testing.T) { func TestGenerateCheckBlessAndJSONDiagnostics(t *testing.T) {
t.Parallel() t.Parallel()
directory := t.TempDir() directory := testpath.TempDir(t)
sourcePath := filepath.Join(directory, "hello.sando") sourcePath := filepath.Join(directory, "hello.sando")
source := "<?sando go\npackage views\nfunc Hello(name string)\n?>\n<p><?= name ?></p>\n" source := "<?sando go\npackage views\nfunc Hello(name string)\n?>\n<p><?= name ?></p>\n"
if err := os.WriteFile(sourcePath, []byte(source), 0o600); err != nil { if err := os.WriteFile(sourcePath, []byte(source), 0o600); err != nil {
+94
View File
@@ -0,0 +1,94 @@
# SPDX-License-Identifier: AGPL-3.0-only
HIM1001
HIM1002
HIM1101
HIM1102
HIM1103
HIM1104
HIM1105
HIM1110
HIM1111
HIM1112
HIM1113
HIM1114
HIM1115
HIM1116
HIM1117
HIM1118
HIM1119
HIM1120
HIM1121
HIM1122
HIM1123
HIM1201
HIM1202
HIM1203
HIM1210
HIM1301
HIM1302
HIM1303
HIM1310
HIM1311
HIM1320
HIM1321
HIM1322
HIM1323
HIM1324
HIM1325
HIM1326
HIM1327
HIM1328
HIM1329
HIM1330
HIM1331
HIM1332
HIM1333
HIM1340
HIM1341
HIM1342
HIM1343
HIM1344
HIM1345
HIM1346
HIM1347
HIM1350
HIM1351
HIM1352
HIM1353
HIM1354
HIM1355
HIM1356
HIM1357
HIM1401
HIM1410
HIM1500
HIM1501
HIM1901
HIM1902
HIM1903
HIM2001
HIM2002
HIM2003
HIM2004
HIM2005
HIM2006
HIM2007
HIM2008
HIM2009
HIM2010
HIM2011
HIM2012
HIM2013
HIM2014
HIM2101
HIM2102
HIM2103
HIM2104
HIM2110
HIM2201
HIM2202
HIM2203
HIM2204
HIM2205
HIM2901
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
// Code generated by himesan; DO NOT EDIT.
// himesan:compiler <compiler-version>
// himesan:runtime-abi <runtime-abi>
// himesan:source-sha256 <lowercase-sha256>
var _ = <sando-import>.ABISandoV1
//line <source-path>:<line>:<column>
+14
View File
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
Sandwich Hime / Hime-san — HTML-first typed components for Go
Usage:
himesan generate [--json] [paths...] generate adjacent .sando.go files
himesan gen [--json] [paths...] alias for generate
himesan check [--json] [paths...] validate sources and committed output without writes
himesan bless [--json] [paths...] friendly read-only alias for check
himesan dev [flags] [package] [-- app-args...] run the loopback last-good supervisor
himesan lsp --stdio run the read-only language server
himesan version [--json] print compiler and runtime ABI versions
Templates use .sando; .san remains exclusively San language source.
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sandwichhime.com/schema/himesan-config-v1.schema.json",
"title": "Hime-san local development configuration v1",
"type": "object",
"additionalProperties": false,
"required": ["version"],
"properties": {
"version": {"const": 1},
"sourceRoots": {
"type": "array",
"items": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000]+$"},
"minItems": 1,
"default": ["."]
},
"goPackage": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000\\r\\n]+$", "default": "."},
"appArgs": {"type": "array", "items": {"type": "string", "pattern": "^[^\\u0000]*$"}, "default": []},
"listenAddressEnv": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
"default": "HIMESAN_LISTEN_ADDR"
},
"healthPath": {"type": "string", "pattern": "^/(?!/)[^?#\\u0000\\r\\n]*$", "default": "/"},
"proxyAddress": {
"type": "string",
"anyOf": [
{"pattern": "^127(?:\\.[0-9]{1,3}){3}:[0-9]{1,5}$"},
{"pattern": "^\\[::1\\]:[0-9]{1,5}$"}
],
"default": "127.0.0.1:7331"
},
"additionalWatchRoots": {
"type": "array",
"items": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000]+$"},
"default": []
}
}
}
@@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sandwichhime.com/schema/himesan-operation-output-v1.schema.json",
"title": "Hime-san generate/check JSON result v1",
"type": "object",
"additionalProperties": false,
"required": ["command", "ok", "result"],
"properties": {
"command": {"enum": ["generate", "check", "bless"]},
"ok": {"type": "boolean"},
"result": {
"type": "object",
"additionalProperties": false,
"required": ["files", "discovered", "changed", "unchanged", "stale", "missing"],
"properties": {
"files": {
"type": ["array", "null"],
"items": {
"type": "object",
"additionalProperties": false,
"required": ["source_path", "output_path", "changed", "stale", "missing"],
"properties": {
"source_path": {"type": "string"},
"output_path": {"type": "string"},
"changed": {"type": "boolean"},
"stale": {"type": "boolean"},
"missing": {"type": "boolean"}
}
}
},
"diagnostics": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["path", "line", "column", "code", "severity", "message"],
"properties": {
"path": {"type": "string"},
"line": {"type": "integer", "minimum": 1},
"column": {"type": "integer", "minimum": 1},
"code": {"type": "string", "pattern": "^HIM[0-9]{4}$"},
"severity": {"enum": ["error", "warning"]},
"message": {"type": "string"}
}
}
},
"discovered": {"type": "integer", "minimum": 0},
"changed": {"type": "integer", "minimum": 0},
"unchanged": {"type": "integer", "minimum": 0},
"stale": {"type": "integer", "minimum": 0},
"missing": {"type": "integer", "minimum": 0}
}
}
}
}
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sandwichhime.com/schema/himesan-version-output-v1.schema.json",
"title": "Hime-san version JSON result v1",
"type": "object",
"additionalProperties": false,
"required": ["compiler", "runtime_abi", "go", "features"],
"properties": {
"compiler": {"type": "string", "minLength": 1},
"runtime_abi": {"const": "sando.v1"},
"go": {"type": "string", "minLength": 1},
"features": {
"type": "array",
"prefixItems": [{"const": "lsp-stdio"}],
"minItems": 1,
"maxItems": 1
}
}
}
+22
View File
@@ -9,6 +9,28 @@ synthetic cases under the published method. Only reproduced improvements become
marketing claims. Microbenchmarks do not justify claims about request marketing claims. Microbenchmarks do not justify claims about request
throughput, database-heavy pages, or whole-application latency. throughput, database-heavy pages, or whole-application latency.
The threshold was fixed before measuring the RC. On each maintained native
platform and toolchain, ten benchmark samples use the exact output-equivalent
`BenchmarkV1Corpus*` pair. Sandwich Hime passes when its median `ns/op` and
`B/op` are each no more than 125% of `html/template`, and its median
allocations/op are no more than two allocations above `html/template`.
Any failed platform/toolchain pair is a material regression. Timing is reviewed
from raw samples rather than enforced in ordinary CI, where host contention
would turn a performance policy into a flaky correctness gate.
Run:
```sh
cd sando
go test -run '^TestBenchmarkCorpusEquivalent$' \
-bench '^BenchmarkV1Corpus' -benchmem -benchtime=2s -count=10
```
`benchmarkSandoComponent` intentionally mirrors generated writer calls and
captures the same typed view used by the parsed standard template. This is a
runtime renderer microbenchmark; it excludes parsing, compiler execution,
HTTP, routing, logging, databases, and deployment.
Benchmark fixtures must be self-contained, synthetic, reviewable, and committed Benchmark fixtures must be self-contained, synthetic, reviewable, and committed
to this repository. Application-specific datasets and deployment measurements to this repository. Application-specific datasets and deployment measurements
belong with their applications and are neither copied here nor treated as core belong with their applications and are neither copied here nor treated as core
+20 -5
View File
@@ -30,13 +30,28 @@ compiler versions; `himesan check` defines whether they are current. The
project makes no compatibility promise for internal packages, development SSE project makes no compatibility promise for internal packages, development SSE
payloads before final v1, or hand-edited generated files. payloads before final v1, or hand-edited generated files.
The v1 compatibility snapshots cover the exported `sando` API and values, CLI
help and exit-code classes, structured operation/version output, diagnostic
codes, `himesan.json`, and generated provenance. English diagnostic wording,
internal packages, temporary paths, and compiler implementation details are not
stable API.
An API deprecated after final v1 remains available for the rest of the v1
major line and may be removed in v2. A security correction may fail closed in
a patch release when retaining old behavior would contradict a published safety
guarantee; that exception receives an advisory and migration note rather than a
silent compatibility claim. Until a broader maintenance policy is announced,
only the latest stable v1 patch and the current prerelease receive fixes.
## Go and platform support ## Go and platform support
The current beta targets Go 1.25 and Go 1.26 on Linux/amd64. Required release The modules retain a `go 1.25` language directive for consumer compatibility.
evidence runs in Linux CI and on Linux deployment hosts. WSL is treated as a The maintained v1 build and verification targets are Linux/amd64 and Apple
Linux development environment. Native Windows, macOS, and other targets are Silicon macOS/arm64 using the pinned patched Go 1.26.7 and Go 1.27.0 toolchains.
not maintained release targets or release blockers; a successful build there Both native targets are release blockers. A sleeping or unavailable Mac delays
is useful portability evidence, not a compatibility promise. A Go or platform the release gate rather than silently converting it into Linux or
cross-compilation evidence. Native Windows, Intel macOS, Linux/arm64, and other
targets may work but are not v1 compatibility promises. A Go or platform
support change is announced in release notes before it takes effect. support change is announced in release notes before it takes effect.
### Historical Beta 1 observations ### Historical Beta 1 observations
+3
View File
@@ -8,6 +8,9 @@ The application must read its listen address from the configured environment var
## `himesan.json` schema version 1 ## `himesan.json` schema version 1
The machine-readable contract is
[`contracts/himesan-config-v1.schema.json`](../contracts/himesan-config-v1.schema.json).
```json ```json
{ {
"version": 1, "version": 1,
+24
View File
@@ -21,3 +21,27 @@ Code families are intentionally coarse compatibility surfaces:
| `HIM29xx` | Boundary warnings | | `HIM29xx` | Boundary warnings |
Scripts should consume the JSON `code`, `severity`, and location fields, not parse English messages. Message wording may improve within a compatible release. Scripts should consume the JSON `code`, `severity`, and location fields, not parse English messages. Message wording may improve within a compatible release.
The exact v1 code inventory is machine-checked against
[`contracts/diagnostic-codes-v1.txt`](../contracts/diagnostic-codes-v1.txt).
Adding, removing, or renumbering a code requires an explicit compatibility
review and snapshot update.
## CLI and structured-output contract
Command exit codes use three classes: `0` for success (including help and
warning-only results), `1` for a completed operation that failed validation or
runtime service, and `2` for invalid command usage or failure to encode the
requested CLI result. `gen` normalizes to `generate`; `bless` remains a named
read-only alias of `check` in structured output.
The v1 JSON shapes are published as closed schemas:
- [`himesan-operation-output-v1.schema.json`](../contracts/himesan-operation-output-v1.schema.json)
for `generate`, `check`, and `bless`;
- [`himesan-version-output-v1.schema.json`](../contracts/himesan-version-output-v1.schema.json)
for `version --json`.
Unknown output fields are not introduced in a compatible v1 patch without an
explicit schema/version decision. Consumers should still ignore English
message wording.
+48 -14
View File
@@ -25,8 +25,9 @@ is retained only as supplementary Linux evidence. Hostnames, network addresses,
account names, private paths, private repository identities, and private commit account names, private paths, private repository identities, and private commit
mappings are intentionally absent from this public ledger. These platform mappings are intentionally absent from this public ledger. These platform
observations are historical evidence, not the current support matrix. observations are historical evidence, not the current support matrix.
Linux/amd64 is now the maintained release target; WSL is a Linux development Linux/amd64 and Darwin/arm64 are now the maintained v1 release targets. This
environment, while native Windows and macOS are not release blockers. section retains historical Beta 1 evidence; the exact RC must supply new native
evidence on both targets. WSL and native Windows are not v1 release blockers.
## Beta 2 compiler publication addendum ## Beta 2 compiler publication addendum
@@ -82,11 +83,11 @@ baseline commit.
| Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found | | Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found |
| Deterministic generation | repeated generate/check/hash/mtime gates | Pass | | Deterministic generation | repeated generate/check/hash/mtime gates | Pass |
| Writer failures | runtime error/short-write/nil-writer tests | Pass | | Writer failures | runtime error/short-write/nil-writer tests | Pass |
| HTML text/attribute/RCDATA escaping | compiler and runtime adversarial cases | Pass for enumerated cases | | HTML text/attribute/RCDATA escaping | compiler/runtime adversarial cases plus the committed `html/template` overlap corpus | Pass for the committed corpus; documented stricter invalid-UTF-8 handling remains intentional |
| URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases | | URL scheme handling | ordinary/trusted URL matrices plus safe, unsafe, and intentionally divergent `html/template` cases | Pass for the committed corpus; control rejection and the explicit `tel` allowlist are documented policy differences |
| Filesystem boundaries | symlink, nested-module, VCS, ownership, stale-output tests | Pass for tested cases; see open findings | | Filesystem boundaries | symlink, nested-module, VCS, ownership, stale-output tests | Pass for tested cases; see open findings |
| Development proxy browser boundary | Host, Origin, Fetch Metadata, CSP, fragment and response tests | Pass for tested cases | | Development proxy browser boundary | Host, Origin, Fetch Metadata, CSP, fragment and response tests | Pass for tested cases |
| Platform behavior | Historical exact-candidate native Windows and executed Linux matrices | Windows/Linux passed for the tested lanes; current releases require Linux/amd64 evidence | | Platform behavior | Historical exact-candidate native Windows and executed Linux matrices | Windows/Linux passed for the tested lanes; the v1 RC requires fresh Linux/amd64 and Darwin/arm64 evidence |
Coverage measures statements executed by tests. It is not branch completeness Coverage measures statements executed by tests. It is not branch completeness
and is not evidence that the executed behavior is secure. and is not evidence that the executed behavior is secure.
@@ -105,7 +106,7 @@ independent audit.
| Windows 11/amd64, NTFS | 1.25.12, 1.26.5 | Native PowerShell verifier with race; root/runtime tests, vet, trimpath build, freshness, two generation passes, process-tree cleanup, watcher boundaries, and temporary consumer compilation | Pass. Symlink-output rejection skipped because the test account lacked symlink privilege; the read-only-directory case is POSIX-only | | Windows 11/amd64, NTFS | 1.25.12, 1.26.5 | Native PowerShell verifier with race; root/runtime tests, vet, trimpath build, freshness, two generation passes, process-tree cleanup, watcher boundaries, and temporary consumer compilation | Pass. Symlink-output rejection skipped because the test account lacked symlink privilege; the read-only-directory case is POSIX-only |
| Ubuntu 20.04/amd64 under WSL2, native ext4 checkout | 1.25.12, 1.26.5 | Race-enabled verifier; root/runtime tests, vet, build, two generation passes, ten focused filesystem cases, five focused development-process/watcher cases, and license check | Pass. This is Linux execution under WSL2, not bare-metal or Linux/arm64 evidence | | Ubuntu 20.04/amd64 under WSL2, native ext4 checkout | 1.25.12, 1.26.5 | Race-enabled verifier; root/runtime tests, vet, build, two generation passes, ten focused filesystem cases, five focused development-process/watcher cases, and license check | Pass. This is Linux execution under WSL2, not bare-metal or Linux/arm64 evidence |
| Linux/amd64 server containers | 1.25.12, 1.26.5 | Earlier pre-beta root/runtime tests, vet, builds, race, licensing, and deterministic generation in sequential isolated official Go containers | Pass on the earlier baseline only. Container resources were capped at 1 CPU and 2 GiB; this is supplementary evidence, not an exact Beta 1 lane or Linux/arm64 evidence | | Linux/amd64 server containers | 1.25.12, 1.26.5 | Earlier pre-beta root/runtime tests, vet, builds, race, licensing, and deterministic generation in sequential isolated official Go containers | Pass on the earlier baseline only. Container resources were capped at 1 CPU and 2 GiB; this is supplementary evidence, not an exact Beta 1 lane or Linux/arm64 evidence |
| macOS | — | Cross-compilation only | No native Beta 1 evidence; not a maintained release target | | macOS | — | Cross-compilation only | No native Beta 1 evidence; Darwin/arm64 becomes a maintained target at the v1 RC and requires fresh evidence |
The generated golden `basic.sando.go` was 1,399 bytes and had SHA-256 The generated golden `basic.sando.go` was 1,399 bytes and had SHA-256
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f` `63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`
@@ -118,7 +119,7 @@ architecture, `go version`, exact command, and a minimal reproduction.
Suspected vulnerabilities use the private route in Suspected vulnerabilities use the private route in
[SECURITY.md](../SECURITY.md). Such reports help find gaps but do not create a [SECURITY.md](../SECURITY.md). Such reports help find gaps but do not create a
support promise; maintainers remain responsible for security triage and fixes support promise; maintainers remain responsible for security triage and fixes
on the supported Linux target. on both supported native targets.
## Security-relevant design evidence ## Security-relevant design evidence
@@ -183,8 +184,13 @@ go test ./internal/compiler -run '^$' \
./scripts/verify-public-install.sh --version v1.0.0-beta.1 ./scripts/verify-public-install.sh --version v1.0.0-beta.1
``` ```
The fuzz targets currently assert process robustness and result bounds. They do Those historical Beta 1 fuzz targets asserted process robustness and result
not yet prove semantic HTML safety. bounds; they did not prove semantic HTML safety. The v1 compiler target now
also asserts deterministic diagnostics and generated Go, valid formatted Go,
source-digest binding, bounded public diagnostic shape, and sanitized source
map directives. A separate runtime target asserts deterministic, fail-closed
URL handling with no partial output. These properties still do not replace the
committed differential corpus or real-browser testing.
## Assessment findings and remediation status ## Assessment findings and remediation status
@@ -207,8 +213,8 @@ and executed Linux matrices, and Windows/macOS cross-compilation on 2026-08-12.
Signed annotated runtime and compiler tags were then published from that commit Signed annotated runtime and compiler tags were then published from that commit
in that order. Fresh runtime-first installation passed through both direct Git in that order. Fresh runtime-first installation passed through both direct Git
resolution and the public Go proxy after normal proxy propagation. Future resolution and the public Go proxy after normal proxy propagation. Future
release decisions use the current Linux-only support policy rather than release decisions require fresh evidence for the maintained Linux/amd64 and
requiring this historical multi-platform campaign. Darwin/arm64 targets rather than reusing this historical campaign.
## Open assurance gaps ## Open assurance gaps
@@ -218,9 +224,10 @@ requiring this historical multi-platform campaign.
- the signed annotated Beta tags and their common peeled commit were verified; - the signed annotated Beta tags and their common peeled commit were verified;
prebuilt-artifact signing, checksums, SBOM, reproducible provenance, and key prebuilt-artifact signing, checksums, SBOM, reproducible provenance, and key
recovery remain incomplete; recovery remain incomplete;
- Linux/arm64 and non-Linux portability are outside the current maintained - Linux/arm64, Darwin/amd64, Windows, and other targets are outside the current
release target; maintained release set;
- browser-parser differential and semantic property testing need expansion; - the exact public candidate still needs the committed real-browser generated
document and development-supervisor campaign on both maintained hosts;
- compiler input size, CPU, and memory have no built-in hard budget; - compiler input size, CPU, and memory have no built-in hard budget;
- filesystem checks do not defend against a hostile local actor racing path - filesystem checks do not defend against a hostile local actor racing path
components between inspection and use; components between inspection and use;
@@ -234,6 +241,33 @@ requiring this historical multi-platform campaign.
- static cycle detection and trust-use warnings are best-effort analyses; and - static cycle detection and trust-use warnings are best-effort analyses; and
- the project has no independent security audit or bug-bounty program. - the project has no independent security audit or bug-bounty program.
## v1 disposition of open gaps
The list above intentionally mixes incomplete release evidence with boundaries
that are not promised by this product. The RC may not convert either category
into vague assurance. The following disposition is explicit and remains
subject to exact-public-candidate review:
| Gap | v1 disposition |
| --- | --- |
| Security mailbox delivery, backup, and recovery | Release blocker. Complete the delivery/reply and recovery drill before RC publication. Encrypted reporting may remain optional if the supported confidential channel and its limit are stated accurately. |
| Artifact signing, provenance, and key recovery | Release blocker. Complete deterministic native artifacts, Developer ID notarization, signed-tag rehearsal, and recovery evidence. |
| Maintained native matrix | Release blocker for Linux/amd64 and Darwin/arm64 only. Other architectures and operating systems are explicitly unsupported, not silently untested promises. |
| Real-browser parser and supervisor evidence | Release blocker. The repository-owned gate covers a generated typed document, parsed structure, hostile-value inertness, and supervisor behavior. Execute it against the exact public candidate on both maintained hosts before publication. |
| Compiler resource budgets | Accepted v1 boundary. The compiler is a trusted local build tool; operating-system and runner limits own CPU, memory, and input quotas. No hostile-input resource guarantee is made. |
| Hostile local filesystem races | Accepted v1 boundary. Symlinks and ownership are checked, but an actor able to mutate the workspace concurrently is outside the trust model. |
| Watcher integrity | Accepted v1 boundary. Watching is development convenience; explicit `check`, generation, Go tests, and builds remain release/deployment authority. |
| Human-readable child diagnostics | Accepted v1 boundary. They are bounded for resources but remain trusted local terminal output, not a sanitized telemetry format. |
| Development CSP rewriting | Accepted v1 boundary. It enables reload on trusted loopback pages and is not production CSP validation. |
| Deliberately detached descendants | Accepted v1 boundary. Ordinary process groups are terminated and waited for; adversarial detachment is outside the trusted-project development model. |
| Render recursion, output, panic, allocation, CPU, and deadlines | Accepted v1 boundary. Components are ordinary trusted Go; applications own recovery, deadlines, and resource policy. |
| Static cycles and trust warnings | Accepted v1 boundary. They are documented best-effort audit hints and never replace Go review/tests or explicit trust decisions. |
| Independent audit and bug bounty | Accepted disclosure, not a security claim. Neither exists for RC. Public tests, threat model, reporting, and correction policy must not be described as an independent audit. |
An accepted boundary is permitted only because matching compatibility, threat
model, and release copy already avoid the stronger promise. Any conflicting
marketing or documentation reopens the item as a release blocker.
## Interpreting this ledger ## Interpreting this ledger
“Pass” means the named command or case produced its expected result in the named “Pass” means the named command or case produced its expected result in the named
+2 -1
View File
@@ -177,7 +177,8 @@ Sandwich Hime does not:
## Open release work ## Open release work
- broaden semantic and browser-parser differential testing; - broaden semantic and browser-parser differential testing;
- execute the Linux/amd64 security and process-lifecycle release matrix; - execute the Linux/amd64 and Darwin/arm64 security and process-lifecycle
release matrices;
- complete signed release provenance, checksums, and SBOM evidence; - complete signed release provenance, checksums, and SBOM evidence;
- test the confidential reporting and signing-key recovery procedures; and - test the confidential reporting and signing-key recovery procedures; and
- close or explicitly accept every finding listed in the evidence ledger before - close or explicitly accept every finding listed in the evidence ledger before
+39 -32
View File
@@ -28,7 +28,7 @@ private history or an indiscriminate Git mirror.
Beta 1 is deliberately earlier than a release candidate. It creates a real, Beta 1 is deliberately earlier than a release candidate. It creates a real,
repeatable install for learners and evaluators without claiming that the final repeatable install for learners and evaluators without claiming that the final
v1 compatibility, Linux release, artifact, signing, or soak gates are complete. v1 compatibility, native release, artifact, signing, or soak gates are complete.
### Demonstrated for Beta 1 ### Demonstrated for Beta 1
@@ -38,8 +38,8 @@ Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
WSL2, with the earlier pre-beta server-container run retained only as WSL2, with the earlier pre-beta server-container run retained only as
supplementary Linux evidence. The same generated golden SHA-256 was observed supplementary Linux evidence. The same generated golden SHA-256 was observed
across the exact Beta Windows and Linux lanes. This is historical evidence, across the exact Beta Windows and Linux lanes. This is historical evidence,
not the current support definition; Linux/amd64 is now the maintained release not the current support definition; Linux/amd64 and Darwin/arm64 are the
target. maintained v1 release targets.
Other demonstrated controls include: Other demonstrated controls include:
@@ -53,15 +53,16 @@ Other demonstrated controls include:
### Not demonstrated yet ### Not demonstrated yet
- final Linux/amd64 release-candidate evidence on the exact candidate; - final Linux/amd64 and Darwin/arm64 release-candidate evidence on the exact
- stable final-v1 API, CLI, schema, diagnostic, and generated snapshots; canonical candidate;
- systematic browser-parser and `html/template` differential testing; - exact-public-candidate execution of the committed real-browser generated
document and development-supervisor gate on both maintained hosts;
- a long semantic fuzz campaign beyond bounded no-panic smoke; - a long semantic fuzz campaign beyond bounded no-panic smoke;
- committed comparative benchmarks and predefined regression thresholds; - exact-candidate comparative benchmark results on both maintained targets;
- complete real-browser development-supervisor evidence;
- deterministic prebuilt archives, checksums, SBOMs, signed binaries, and - deterministic prebuilt archives, checksums, SBOMs, signed binaries, and
tested signing/recovery procedures; or tested signing/recovery procedures; or
- independently reproduced Linux release artifacts, checksums, and SBOMs. - independently reproduced Linux and macOS release artifacts, checksums, and
SBOMs.
## Beta 1 publication lane ## Beta 1 publication lane
@@ -82,45 +83,50 @@ dependency, and its interfaces may change.
## Milestone 1: contract freeze ## Milestone 1: contract freeze
Required before security/Linux release-candidate work is declared complete: Required before security/native release-candidate work is declared complete:
- [ ] Decide and specify whether generic component function signatures are v1. - [x] Specify generic component function signatures as ordinary Go-backed v1
- [ ] Inventory and freeze every exported `sando` symbol, trusted type, APIs and compile them in a temporary consumer module.
- [x] Inventory and freeze every exported `sando` symbol, trusted type,
sentinel error, concrete error field, helper, and ABI marker. sentinel error, concrete error field, helper, and ABI marker.
- [ ] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas, - [x] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas,
`himesan.json` schema, and generated provenance fields. `himesan.json` schema, and generated provenance fields.
- [ ] Specify nil/stringification behavior, supported HTML-context matrix, - [x] Specify nil/stringification behavior, supported HTML-context matrix,
component trust boundary, URL semantics, and explicit unsupported cases. component trust boundary, URL semantics, and explicit unsupported cases.
- [ ] Add machine-checked public API, CLI, diagnostic, schema, and generated - [x] Add machine-checked public API, CLI, diagnostic, schema, and generated
output compatibility snapshots. output compatibility snapshots.
- [ ] Define the v1 deprecation and security-support policy. - [x] Define the v1 deprecation and security-support policy.
## Milestone 2: security and Linux release evidence ## Milestone 2: security and native release evidence
- [ ] Run the minimum supported Go line and the latest two stable Go lines on - [ ] Run the pinned patched Go 1.26 and Go 1.27 lines on Linux/amd64 and
Linux/amd64 runners and a Linux deployment-class host. native Darwin/arm64 runners, plus a Linux deployment-class host.
- [ ] Prove identical generated bytes across those Linux lanes and exercise - [ ] Prove identical generated bytes across both native platforms and exercise
path, replacement, permission, race, process-tree, and watcher behavior. path, replacement, permission, race, process-tree, and watcher behavior.
- [ ] Build a systematic differential corpus against Go's documented - [x] Build a systematic differential corpus against Go's documented
`html/template` safety baseline for overlapping supported contexts. `html/template` safety baseline for overlapping supported contexts.
- [ ] Parse representative outputs in real browsers and test structure/code - [x] Commit a real-browser gate that generates and builds a representative
invariants rather than only byte equality. typed document, then tests parsed structure and hostile-value inertness in
Chrome rather than relying only on byte equality.
- [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports, - [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports,
paths, source maps, URL normalization, and filesystem operations. paths, source maps, URL normalization, and filesystem operations.
- [ ] Resolve or explicitly accept every open item in - [ ] Resolve or explicitly accept every open item in
`SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee. `SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee.
- [ ] Test delivery and reply through `security@sandwichhime.com`. - [ ] Test delivery and reply through `security@sandwichhime.com`.
- [ ] Define severity, advisory, retraction, and CVE-request handling. - [x] Define severity, advisory, retraction, and CVE-request handling.
## Milestone 3: measured performance and development UX ## Milestone 3: measured performance and development UX
- [ ] Commit a synthetic, repository-owned benchmark corpus comparing - [x] Commit a synthetic, repository-owned benchmark corpus comparing
equivalent typed views and output with `html/template`. equivalent typed views and output with `html/template`.
- [ ] Define “no material regression” before measuring the release candidate; - [x] Define “no material regression” before measuring the release candidate.
publish hardware, OS, Go version, commands, samples, allocations, and output - [ ] Measure the exact release candidate and publish hardware, OS, Go version,
equivalence with every result. commands, samples, allocations, and output equivalence with every result.
- [ ] Test SSE reconnect, reload, diagnostic overlays, CSP changes, fragment/API - [x] Commit a real-browser development-supervisor gate covering SSE
exclusions, caching, and child cleanup in a real browser on supported hosts. diagnostics, reload, CSP behavior, fragment/API exclusions, caching, and
child cleanup.
- [ ] Execute the committed browser gate on the exact public candidate on both
maintained hosts and preserve the browser identities and results.
- [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced - [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced
reliably instead of substituting prose for a test. reliably instead of substituting prose for a test.
@@ -134,7 +140,7 @@ Required before security/Linux release-candidate work is declared complete:
and source/build provenance from a clean sanitized canonical checkout. and source/build provenance from a clean sanitized canonical checkout.
- [ ] Test release-key backup and two-person recovery for Gitea, domains, - [ ] Test release-key backup and two-person recovery for Gitea, domains,
signing material, and publication instructions. signing material, and publication instructions.
- [ ] Make evidence gates validate content and commit identity rather than only - [x] Make evidence gates validate content and commit identity rather than only
the presence of non-empty files. the presence of non-empty files.
- [ ] Rehearse runtime-first publication and rollback without creating public - [ ] Rehearse runtime-first publication and rollback without creating public
semver tags. semver tags.
@@ -145,7 +151,8 @@ Required before security/Linux release-candidate work is declared complete:
2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same 2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same
reviewed public Gitea commit. reviewed public Gitea commit.
3. Verify documented installs through fresh `GOPROXY=direct` and 3. Verify documented installs through fresh `GOPROXY=direct` and
`proxy.golang.org` caches on supported Go versions under Linux/amd64. `proxy.golang.org` caches on supported Go versions under Linux/amd64 and
Darwin/arm64.
4. Run the complete evidence suite again from the exact public commit. 4. Run the complete evidence suite again from the exact public commit.
5. Operate the official Sandwich Hime website on the RC runtime for a 14-day 5. Operate the official Sandwich Hime website on the RC runtime for a 14-day
observation period with no unresolved Hime render, security, accessibility, observation period with no unresolved Hime render, security, accessibility,
+138
View File
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"testing"
)
var diagnosticCodePattern = regexp.MustCompile(`^HIM[0-9]{4}$`)
func TestV1DiagnosticCodeContract(t *testing.T) {
t.Parallel()
directory := packageDirectory(t)
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatal(err)
}
codes := make(map[string]struct{})
fileSet := token.NewFileSet()
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
parsed, err := parser.ParseFile(fileSet, filepath.Join(directory, entry.Name()), nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", entry.Name(), err)
}
ast.Inspect(parsed, func(node ast.Node) bool {
literal, ok := node.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return true
}
value, err := strconv.Unquote(literal.Value)
if err == nil && diagnosticCodePattern.MatchString(value) {
codes[value] = struct{}{}
}
return true
})
}
actual := make([]string, 0, len(codes))
for code := range codes {
actual = append(actual, code)
}
sort.Strings(actual)
assertContractFile(t, filepath.Join(directory, "..", "..", "contracts", "diagnostic-codes-v1.txt"), strings.Join(actual, "\n")+"\n")
}
func TestV1GeneratedProvenanceContract(t *testing.T) {
t.Parallel()
compiled, diagnostics := Compile("views/generic.sando", []byte(`<?sando go
package views
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`))
assertNoErrorDiagnostics(t, diagnostics)
lines := strings.Split(string(compiled.Code), "\n")
if len(lines) < 4 {
t.Fatalf("generated header has %d lines", len(lines))
}
actual := []string{lines[0]}
if !strings.HasPrefix(lines[1], "// himesan:compiler ") {
t.Fatalf("compiler provenance line = %q", lines[1])
}
actual = append(actual, "// himesan:compiler <compiler-version>")
if !strings.HasPrefix(lines[2], "// himesan:runtime-abi ") {
t.Fatalf("runtime provenance line = %q", lines[2])
}
actual = append(actual, "// himesan:runtime-abi <runtime-abi>")
if !regexp.MustCompile(`^// himesan:source-sha256 [0-9a-f]{64}$`).MatchString(lines[3]) {
t.Fatalf("source provenance line = %q", lines[3])
}
actual = append(actual, "// himesan:source-sha256 <lowercase-sha256>")
generated := string(compiled.Code)
if !regexp.MustCompile(`(?m)^var _ = [A-Za-z_][A-Za-z0-9_]*\.ABISandoV1$`).MatchString(generated) {
t.Fatal("generated output is missing the compile-time ABI marker")
}
actual = append(actual, "var _ = <sando-import>.ABISandoV1")
if !regexp.MustCompile(`(?m)^//line [^\r\n]+:[1-9][0-9]*:[1-9][0-9]*$`).MatchString(generated) {
t.Fatal("generated output is missing source mappings")
}
actual = append(actual, "//line <source-path>:<line>:<column>")
assertContractFile(t, filepath.Join(packageDirectory(t), "..", "..", "contracts", "generated-provenance-v1.txt"), strings.Join(actual, "\n")+"\n")
}
func TestV1GenericComponentSignature(t *testing.T) {
t.Parallel()
source := []byte(`<?sando go
package views
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
compiled, diagnostics := Compile("views/list.sando", source)
assertNoErrorDiagnostics(t, diagnostics)
if !bytes.Contains(compiled.Code, []byte("func List[T ~string](values []T)")) {
t.Fatalf("generic signature was not preserved:\n%s", compiled.Code)
}
analyses := AnalyzeSources(context.Background(), []SourceInput{{Path: "views/list.sando", Source: source}})
if len(analyses) != 1 {
t.Fatalf("analysis count = %d, want 1", len(analyses))
}
analysis := analyses[0]
if analysis.TypeParams != "[T ~string]" || analysis.Params != "(values []T)" || analysis.Signature != "func List[T ~string](values []T)" {
t.Fatalf("generic analysis contract = %#v", analysis)
}
}
func packageDirectory(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return filepath.Dir(file)
}
func assertContractFile(t *testing.T, path, actual string) {
t.Helper()
expected, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
expected = bytes.TrimPrefix(expected, []byte("# SPDX-License-Identifier: AGPL-3.0-only\n\n"))
if string(expected) != actual {
t.Fatalf("contract drift in %s\n--- expected ---\n%s--- actual ---\n%s", path, expected, actual)
}
}
+10 -1
View File
@@ -128,6 +128,9 @@ func TestRCDATACannotBeBypassedByTrustedHTML(t *testing.T) {
view := View{Name: "title", URL: "/", JS: sando.TrustJS(""), HTML: sando.TrustHTML("</textarea><script>bad()</script>")} view := View{Name: "title", URL: "/", JS: sando.TrustJS(""), HTML: sando.TrustHTML("</textarea><script>bad()</script>")}
if err := sando.Render(context.Background(), &output, Page(view)); err != nil { t.Fatal(err) } if err := sando.Render(context.Background(), &output, Page(view)); err != nil { t.Fatal(err) }
if strings.Contains(output.String(), "</textarea><script>") { t.Fatalf("RCDATA boundary escaped: %s", output.String()) } if strings.Contains(output.String(), "</textarea><script>") { t.Fatalf("RCDATA boundary escaped: %s", output.String()) }
output.Reset()
if err := sando.Render(context.Background(), &output, List([]string{"one", "two"})); err != nil { t.Fatal(err) }
if output.String() != "\n<ul><li>one</li><li>two</li></ul>" { t.Fatalf("generic component output: %q", output.String()) }
} }
`) `)
templatePath := filepath.Join(directory, "page.sando") templatePath := filepath.Join(directory, "page.sando")
@@ -141,7 +144,13 @@ func Page(view View)
<script><?= view.JS ?></script> <script><?= view.JS ?></script>
<textarea><?= view.HTML ?></textarea> <textarea><?= view.HTML ?></textarea>
</body></html>`) </body></html>`)
result, err := Generate(context.Background(), []string{templatePath}) genericPath := filepath.Join(directory, "list.sando")
mustWrite(t, genericPath, `<?sando go
package generated
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
result, err := Generate(context.Background(), []string{templatePath, genericPath})
if err != nil { if err != nil {
t.Fatalf("Generate failed: %v (%v)", err, result.Diagnostics) t.Fatalf("Generate failed: %v (%v)", err, result.Diagnostics)
} }
+69 -4
View File
@@ -2,7 +2,16 @@
package compiler package compiler
import "testing" import (
"bytes"
"crypto/sha256"
"fmt"
"go/parser"
"go/token"
"reflect"
"strings"
"testing"
)
func FuzzCompileNeverPanics(f *testing.F) { func FuzzCompileNeverPanics(f *testing.F) {
for _, seed := range []string{ for _, seed := range []string{
@@ -14,13 +23,69 @@ func FuzzCompileNeverPanics(f *testing.F) {
"<?sando go\npackage p\nfunc F(v string)\n?>\n<script><!--<script></script>\n<?= v ?>\n<!--\n</script>\n-->", "<?sando go\npackage p\nfunc F(v string)\n?>\n<script><!--<script></script>\n<?= v ?>\n<!--\n</script>\n-->",
"\xef\xbb\xbf\r\n<?sando go\r\npackage p\r\nfunc F()\r\n?>\r\n<p>x</p>", "\xef\xbb\xbf\r\n<?sando go\r\npackage p\r\nfunc F()\r\n?>\r\n<p>x</p>",
} { } {
f.Add(seed) f.Add(seed, "fuzz.sando")
}
f.Add("<?sando go\npackage p\nfunc F()\n?>\n<p>x</p>", "path%with\ncontrols\x00.sando")
f.Fuzz(func(t *testing.T, source, mapping string) {
if len(source) > 64<<10 || len(mapping) > 4<<10 {
t.Skip()
}
input := []byte(source)
before := append([]byte(nil), input...)
first, firstDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
second, secondDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
if !bytes.Equal(input, before) {
t.Fatal("compiler modified its source input")
}
if !reflect.DeepEqual(firstDiagnostics, secondDiagnostics) ||
first.SourcePath != second.SourcePath || first.OutputPath != second.OutputPath ||
first.Package != second.Package || first.Component != second.Component ||
first.Digest != second.Digest || !bytes.Equal(first.Code, second.Code) {
t.Fatal("repeated in-memory compilation was not deterministic")
}
for _, diagnostic := range firstDiagnostics {
if diagnostic.Path != "fuzz.sando" || diagnostic.Line < 1 || diagnostic.Column < 1 {
t.Fatalf("diagnostic has an invalid location: %#v", diagnostic)
}
if !validDiagnosticCode(diagnostic.Code) || strings.TrimSpace(diagnostic.Message) != diagnostic.Message || diagnostic.Message == "" {
t.Fatalf("diagnostic violates the public shape: %#v", diagnostic)
}
if diagnostic.Severity != SeverityError && diagnostic.Severity != SeverityWarning {
t.Fatalf("diagnostic has an invalid severity: %#v", diagnostic)
}
}
if len(first.Code) == 0 {
return
}
if first.SourcePath != "fuzz.sando" || first.OutputPath != "fuzz.sando.go" {
t.Fatalf("compiled paths are invalid: %#v", first)
}
if first.Digest != fmt.Sprintf("%x", sha256.Sum256(input)) {
t.Fatalf("source digest is not bound to the exact input: %s", first.Digest)
}
if _, err := parser.ParseFile(token.NewFileSet(), "fuzz.sando.go", first.Code, parser.AllErrors); err != nil {
t.Fatalf("successful compilation produced invalid Go: %v\n%s", err, first.Code)
}
for _, line := range bytes.Split(first.Code, []byte{'\n'}) {
if bytes.HasPrefix(line, []byte("//line ")) && (bytes.ContainsAny(line, "\r\x00") || bytes.Count(line, []byte(":")) < 2) {
t.Fatalf("source-map directive was not safely encoded: %q", line)
}
} }
f.Fuzz(func(t *testing.T, source string) {
_, _ = Compile("fuzz.sando", []byte(source))
}) })
} }
func validDiagnosticCode(code string) bool {
if len(code) != 7 || !strings.HasPrefix(code, "HIM") {
return false
}
for _, digit := range code[3:] {
if digit < '0' || digit > '9' {
return false
}
}
return true
}
func FuzzGoDelimiterNeverPanics(f *testing.F) { func FuzzGoDelimiterNeverPanics(f *testing.F) {
for _, seed := range []string{`?>`, `"?>" ?>`, "`?>` ?>", `/* ?> */ ?>`, "// ?>\n?>", `'?' ?>`} { for _, seed := range []string{`?>`, `"?>" ?>`, "`?>` ?>", `/* ?> */ ?>`, "// ?>\n?>", `'?' ?>`} {
f.Add(seed, uint8(0)) f.Add(seed, uint8(0))
+467
View File
@@ -0,0 +1,467 @@
// SPDX-License-Identifier: AGPL-3.0-only
//go:build himesan_browser_evidence
package devserver
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gamertan.com/sandwich-hime/internal/compiler"
)
const realBrowserTimeout = 20 * time.Second
func TestRealBrowserDevelopmentClient(t *testing.T) {
chrome := os.Getenv("HIMESAN_CHROME")
if chrome == "" {
t.Fatal("HIMESAN_CHROME must name the reviewed Chrome or Chromium executable")
}
if info, err := os.Stat(chrome); err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
t.Fatalf("HIMESAN_CHROME is not an executable regular file: %q", chrome)
}
t.Run("diagnostic overlay and CSP", func(t *testing.T) {
harness := newRealBrowserHarness(t)
document := runRealBrowser(t, chrome, harness.url("/"), "browser evidence diagnostic", func() {
waitForRealBrowserSubscriber(t, harness.hub)
harness.hub.publish(Event{
Type: "diagnostic", Phase: "generate", Message: "browser evidence diagnostic",
Diagnostics: []Diagnostic{{Path: "views/home.sando", Line: 7, Column: 3, Code: "HIM1300", Message: "deliberate evidence fixture"}},
})
})
for _, want := range []string{"id=\"__himesan_overlay\"", "browser evidence diagnostic", "views/home.sando:7:3 [HIM1300]"} {
if !strings.Contains(document, want) {
t.Fatalf("browser DOM lacks %q:\n%s", want, document)
}
}
})
t.Run("reload", func(t *testing.T) {
harness := newRealBrowserHarness(t)
document := runRealBrowser(t, chrome, harness.url("/"), "version two", func() {
waitForRealBrowserSubscriber(t, harness.hub)
harness.version.Store(2)
harness.hub.publish(Event{Type: "reload", Phase: "serve", Message: "healthy application activated"})
})
if !strings.Contains(document, `<main id="page-version">version two</main>`) {
t.Fatalf("browser did not reload the selected document:\n%s", document)
}
})
t.Run("fragment and API exclusions", func(t *testing.T) {
harness := newRealBrowserHarness(t)
for _, path := range []string{"/fragment", "/api"} {
document := runRealBrowser(t, chrome, harness.url(path), "", nil)
if strings.Contains(document, "data-himesan-reload") || strings.Contains(document, "__himesan_overlay") {
t.Fatalf("development client leaked into %s:\n%s", path, document)
}
}
})
t.Run("generated document parsing", func(t *testing.T) {
root := writeGeneratedBrowserApplication(t)
cfg := DefaultConfig()
cfg.ProxyAddress = "127.0.0.1:0"
cfg.HealthPath = "/healthz"
supervisor, err := New(Options{
RootDir: root,
Config: cfg,
Generate: func(ctx context.Context) error {
_, generateErr := compiler.Generate(ctx, []string{root})
return generateErr
},
CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 30 * time.Second,
Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second,
StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runResult := make(chan error, 1)
go func() { runResult <- supervisor.Run(ctx) }()
proxyAddress := waitForProxyAddress(t, supervisor)
waitForBody(t, "http://"+proxyAddress+"/", "generated-browser-document")
document := runRealBrowser(t, chrome, "http://"+proxyAddress+"/", "generated-browser-document", nil)
for _, want := range []string{
`<main id="generated-browser-document"`,
`<h1 id="title">&lt;unsafe&gt; &amp; "quoted"</h1>`,
`<textarea id="note">&lt;/textarea&gt;&lt;script id="attacker"&gt;window.evidenceFailed=true&lt;/script&gt;</textarea>`,
`<a id="destination" href="/safe?q=a&amp;b=c">Open</a>`,
`<table id="table"><tbody><tr><td id="cell">&lt;unsafe&gt; &amp; "quoted"</td></tr></tbody></table>`,
} {
if !strings.Contains(document, want) {
t.Fatalf("generated browser DOM lacks %q:\n%s", want, document)
}
}
if strings.Contains(document, `<script id="attacker">`) {
t.Fatalf("hostile RCDATA became executable structure:\n%s", document)
}
cancel()
select {
case err := <-runResult:
if err != nil {
t.Fatalf("stop generated-document supervisor: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("generated-document supervisor did not stop")
}
})
}
func writeGeneratedBrowserApplication(t *testing.T) string {
t.Helper()
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatalf("resolve test-owned browser fixture: %v", err)
}
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("locate browser evidence source")
}
repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", ".."))
files := map[string]string{
"go.mod": fmt.Sprintf("module example.test/himesan-browser-evidence\n\ngo 1.25\n\nrequire gamertan.com/sandwich-hime/sando v0.0.0\nreplace gamertan.com/sandwich-hime/sando => %s/sando\n", filepath.ToSlash(repositoryRoot)),
"page.sando": `<?sando go
package main
func Page(title string, note string, destination string)
?>
<!doctype html>
<html><head><title><?= title ?></title></head><body>
<main id="generated-browser-document" data-note="<?= note ?>">
<h1 id="title"><?= title ?></h1>
<textarea id="note"><?= note ?></textarea>
<a id="destination" href="<?= destination ?>">Open</a>
<table id="table"><tbody><tr><td id="cell"><?= title ?></td></tr></tbody></table>
</main>
</body></html>
`,
"main.go": `package main
import (
"net/http"
"os"
"gamertan.com/sandwich-hime/sando"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
mux.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := sando.Render(request.Context(), w, Page(
` + "`<unsafe> & \"quoted\"`" + `,
` + "`</textarea><script id=\"attacker\">window.evidenceFailed=true</script>`" + `,
"/safe?q=a&b=c",
)); err != nil {
panic(err)
}
})
if err := http.ListenAndServe(os.Getenv("HIMESAN_LISTEN_ADDR"), mux); err != nil {
panic(err)
}
}
`,
}
for name, contents := range files {
if err := os.WriteFile(filepath.Join(root, name), []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
if _, err := compiler.Generate(context.Background(), []string{root}); err != nil {
t.Fatalf("generate browser evidence fixture: %v", err)
}
return root
}
type realBrowserHarness struct {
hub *eventHub
version atomic.Int32
upstream *httptest.Server
proxy *httptest.Server
}
func newRealBrowserHarness(t *testing.T) *realBrowserHarness {
t.Helper()
harness := &realBrowserHarness{hub: newEventHub()}
harness.version.Store(1)
harness.upstream = httptest.NewServer(http.HandlerFunc(harness.serveApplication))
development := newDevelopmentProxy(harness.hub)
upstreamURL, err := url.Parse(harness.upstream.URL)
if err != nil {
t.Fatal(err)
}
if err := development.setTarget(upstreamURL.Host); err != nil {
t.Fatal(err)
}
harness.proxy = httptest.NewServer(development)
proxyURL, err := url.Parse(harness.proxy.URL)
if err != nil {
t.Fatal(err)
}
if err := development.setAuthority(proxyURL.Host); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
harness.hub.close()
harness.proxy.Close()
harness.upstream.Close()
})
return harness
}
func (h *realBrowserHarness) url(path string) string {
return h.proxy.URL + path
}
func (h *realBrowserHarness) serveApplication(w http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/":
version := "one"
if h.version.Load() == 2 {
version = "two"
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'none'; script-src-elem 'none'; connect-src 'none'")
fmt.Fprintf(w, `<!doctype html><html><body><main id="page-version">version %s</main></body></html>`, version)
case "/fragment":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `<p id="fragment">fragment only</p>`)
case "/api":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"kind":"api","ok":true}`)
default:
http.NotFound(w, request)
}
}
func runRealBrowser(t *testing.T, chrome, target, want string, afterStart func()) string {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), realBrowserTimeout)
defer cancel()
toBrowserRead, toBrowserWrite, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
fromBrowserRead, fromBrowserWrite, err := os.Pipe()
if err != nil {
_ = toBrowserRead.Close()
_ = toBrowserWrite.Close()
t.Fatal(err)
}
defer toBrowserWrite.Close()
defer fromBrowserRead.Close()
stderrPath := filepath.Join(t.TempDir(), "chrome.stderr")
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
t.Fatal(err)
}
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
_ = stderr.Close()
t.Fatal(err)
}
command := exec.CommandContext(ctx, chrome,
"--headless=new",
"--disable-background-networking",
"--disable-component-update",
"--disable-default-apps",
"--disable-sync",
"--metrics-recording-only",
"--no-first-run",
"--no-default-browser-check",
"--user-data-dir="+t.TempDir(),
"--remote-debugging-pipe",
"about:blank",
)
command.ExtraFiles = []*os.File{toBrowserRead, fromBrowserWrite}
command.Stdout = devNull
command.Stderr = stderr
command.WaitDelay = 2 * time.Second
if err := command.Start(); err != nil {
_ = toBrowserRead.Close()
_ = fromBrowserWrite.Close()
_ = stderr.Close()
_ = devNull.Close()
t.Fatalf("start real browser: %v", err)
}
_ = toBrowserRead.Close()
_ = fromBrowserWrite.Close()
client := &devToolsPipe{reader: bufio.NewReader(fromBrowserRead), writer: toBrowserWrite}
targetResult := client.call(t, "", "Target.createTarget", map[string]any{"url": target})
var created struct {
TargetID string `json:"targetId"`
}
if err := json.Unmarshal(targetResult, &created); err != nil || created.TargetID == "" {
t.Fatalf("decode Chrome target: %v", err)
}
attachResult := client.call(t, "", "Target.attachToTarget", map[string]any{"targetId": created.TargetID, "flatten": true})
var attached struct {
SessionID string `json:"sessionId"`
}
if err := json.Unmarshal(attachResult, &attached); err != nil || attached.SessionID == "" {
t.Fatalf("decode Chrome session: %v", err)
}
client.call(t, attached.SessionID, "Runtime.enable", map[string]any{})
waitForBrowserDOM(t, client, attached.SessionID, "")
if afterStart != nil {
afterStart()
}
document := waitForBrowserDOM(t, client, attached.SessionID, want)
_ = client.callIgnoringClose("", "Browser.close", map[string]any{})
waitResult := make(chan error, 1)
go func() { waitResult <- command.Wait() }()
select {
case err := <-waitResult:
if err != nil && !errors.Is(err, exec.ErrWaitDelay) {
_ = stderr.Close()
contents, _ := os.ReadFile(stderrPath)
t.Fatalf("real browser failed: %v; stderr=%s", err, boundedBrowserOutput(string(contents)))
}
case <-time.After(5 * time.Second):
_ = command.Process.Kill()
t.Fatal("real browser did not exit after Browser.close")
}
_ = stderr.Close()
_ = devNull.Close()
return document
}
type devToolsPipe struct {
reader *bufio.Reader
writer io.Writer
nextID int
}
type devToolsEnvelope struct {
ID int `json:"id"`
Result json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (c *devToolsPipe) call(t *testing.T, session, method string, parameters map[string]any) json.RawMessage {
t.Helper()
c.nextID++
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
if session != "" {
request["sessionId"] = session
}
contents, err := json.Marshal(request)
if err != nil {
t.Fatal(err)
}
contents = append(contents, 0)
if _, err := c.writer.Write(contents); err != nil {
t.Fatalf("write Chrome DevTools request: %v", err)
}
for {
message, err := c.reader.ReadBytes(0)
if err != nil {
t.Fatalf("read Chrome DevTools response: %v", err)
}
if len(message) > 4<<20 {
t.Fatal("Chrome DevTools response exceeds 4 MiB")
}
var response devToolsEnvelope
if err := json.Unmarshal(bytes.TrimSuffix(message, []byte{0}), &response); err != nil {
t.Fatalf("decode Chrome DevTools response: %v", err)
}
if response.ID != c.nextID {
continue
}
if response.Error != nil {
t.Fatalf("Chrome DevTools %s failed (%d): %s", method, response.Error.Code, response.Error.Message)
}
return response.Result
}
}
func (c *devToolsPipe) callIgnoringClose(session, method string, parameters map[string]any) error {
c.nextID++
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
if session != "" {
request["sessionId"] = session
}
contents, err := json.Marshal(request)
if err != nil {
return err
}
_, err = c.writer.Write(append(contents, 0))
return err
}
func waitForBrowserDOM(t *testing.T, client *devToolsPipe, session, want string) string {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
var last string
for time.Now().Before(deadline) {
result := client.call(t, session, "Runtime.evaluate", map[string]any{
"expression": "document.documentElement && document.documentElement.outerHTML",
"returnByValue": true,
})
var evaluated struct {
Result struct {
Type string `json:"type"`
Value string `json:"value"`
} `json:"result"`
}
if err := json.Unmarshal(result, &evaluated); err == nil && evaluated.Result.Type == "string" {
last = evaluated.Result.Value
if last != "" && (want == "" || strings.Contains(last, want)) {
return last
}
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("browser DOM did not contain %q; last DOM:\n%s", want, last)
return ""
}
func waitForRealBrowserSubscriber(t *testing.T, hub *eventHub) {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
hub.mu.Lock()
count := len(hub.subscribers)
hub.mu.Unlock()
if count != 0 {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("real browser did not establish the same-origin event stream")
}
func boundedBrowserOutput(value string) string {
const limit = 4096
if len(value) <= limit {
return value
}
return value[:limit] + "\n[Chrome output truncated]"
}
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: AGPL-3.0-only
package devserver
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
)
func TestV1ConfigSchemaMatchesConfigTypeAndDefaults(t *testing.T) {
t.Parallel()
contents, err := os.ReadFile(filepath.Join("..", "..", "contracts", "himesan-config-v1.schema.json"))
if err != nil {
t.Fatal(err)
}
var schema struct {
AdditionalProperties bool `json:"additionalProperties"`
Required []string `json:"required"`
Properties map[string]json.RawMessage `json:"properties"`
}
if err := json.Unmarshal(contents, &schema); err != nil {
t.Fatal(err)
}
if schema.AdditionalProperties {
t.Fatal("v1 config schema must reject unknown fields")
}
if !reflect.DeepEqual(schema.Required, []string{"version"}) {
t.Fatalf("required config fields = %v, want [version]", schema.Required)
}
typeOfConfig := reflect.TypeOf(Config{})
fields := make([]string, 0, typeOfConfig.NumField())
for index := 0; index < typeOfConfig.NumField(); index++ {
name := strings.Split(typeOfConfig.Field(index).Tag.Get("json"), ",")[0]
fields = append(fields, name)
}
sort.Strings(fields)
properties := make([]string, 0, len(schema.Properties))
for name := range schema.Properties {
properties = append(properties, name)
}
sort.Strings(properties)
if !reflect.DeepEqual(fields, properties) {
t.Fatalf("Config JSON fields %v do not match schema properties %v", fields, properties)
}
defaults := DefaultConfig()
wantDefaults := map[string]any{
"version": float64(defaults.Version),
"sourceRoots": []any{"."},
"goPackage": defaults.GoPackage,
"appArgs": []any{},
"listenAddressEnv": defaults.ListenAddressEnv,
"healthPath": defaults.HealthPath,
"proxyAddress": defaults.ProxyAddress,
"additionalWatchRoots": []any{},
}
for name, want := range wantDefaults {
var property map[string]any
if err := json.Unmarshal(schema.Properties[name], &property); err != nil {
t.Fatal(err)
}
if name == "version" {
if !reflect.DeepEqual(property["const"], want) {
t.Fatalf("schema %s const = %#v, want %#v", name, property["const"], want)
}
continue
}
if !reflect.DeepEqual(property["default"], want) {
t.Fatalf("schema %s default = %#v, want %#v", name, property["default"], want)
}
}
}
+4 -1
View File
@@ -81,7 +81,7 @@ func (c *candidateProcess) cleanupProcessTree() error {
processTree := c.processTree processTree := c.processTree
c.processTree = 0 c.processTree = 0
c.mu.Unlock() c.mu.Unlock()
return cleanupProcess(c.command, processTree) return acceptableStopError(cleanupProcess(c.command, processTree))
} }
func (c *candidateProcess) stop(ctx context.Context) error { func (c *candidateProcess) stop(ctx context.Context) error {
@@ -119,6 +119,9 @@ func acceptableStopError(err error) error {
if err == nil { if err == nil {
return nil return nil
} }
if errors.Is(err, os.ErrProcessDone) {
return nil
}
var exitError *exec.ExitError var exitError *exec.ExitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
return nil return nil
+7
View File
@@ -87,6 +87,13 @@ func TestTaskkillArguments(t *testing.T) {
} }
} }
func TestAlreadyFinishedIsAnAcceptableStopResult(t *testing.T) {
t.Parallel()
if err := acceptableStopError(os.ErrProcessDone); err != nil {
t.Fatalf("already-finished process was treated as a stop failure: %v", err)
}
}
func TestManagedProcessHelper(t *testing.T) { func TestManagedProcessHelper(t *testing.T) {
switch os.Getenv("HIMESAN_PROCESS_HELPER") { switch os.Getenv("HIMESAN_PROCESS_HELPER") {
case "": case "":
+19 -5
View File
@@ -18,6 +18,8 @@ import (
"time" "time"
) )
const integrationCandidateStartupTimeout = 3 * time.Second
func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) { func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("integration test builds temporary Go applications") t.Skip("integration test builds temporary Go applications")
@@ -50,7 +52,10 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
PollInterval: 25 * time.Millisecond, PollInterval: 25 * time.Millisecond,
Debounce: 25 * time.Millisecond, Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second, BuildTimeout: 30 * time.Second,
StartupTimeout: 750 * time.Millisecond, // Keep the production default unchanged. Native release runners may
// compile another Go line concurrently, so this integration test gives
// the temporary child enough time to be scheduled under honest load.
StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second, ShutdownTimeout: 2 * time.Second,
}) })
if err != nil { if err != nil {
@@ -128,7 +133,7 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
PollInterval: 30 * time.Second, PollInterval: 30 * time.Second,
Debounce: 25 * time.Millisecond, Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second, BuildTimeout: 30 * time.Second,
StartupTimeout: time.Second, StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second, ShutdownTimeout: 2 * time.Second,
}) })
if err != nil { if err != nil {
@@ -140,7 +145,11 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
t.Cleanup(cancel) t.Cleanup(cancel)
proxyAddress := waitForProxyAddress(t, supervisor) proxyAddress := waitForProxyAddress(t, supervisor)
waitForBody(t, "http://"+proxyAddress+"/", "short lived") // Observe the supervisor's ordered activation event instead of racing an
// HTTP request against the deliberately short-lived child. Other tests
// prove proxy serving; this test proves that an activated child which exits
// clears its selected target under scheduler and compiler load.
waitForEvent(t, events, "reload", "serve")
waitForPhase(t, events, "run") waitForPhase(t, events, "run")
if target := supervisor.proxy.target.Load(); target != nil { if target := supervisor.proxy.target.Load(); target != nil {
t.Fatalf("proxy retained exited upstream %v", target) t.Fatalf("proxy retained exited upstream %v", target)
@@ -290,17 +299,22 @@ func main() {
} }
func waitForPhase(t *testing.T, events <-chan Event, phase string) { func waitForPhase(t *testing.T, events <-chan Event, phase string) {
t.Helper()
waitForEvent(t, events, "diagnostic", phase)
}
func waitForEvent(t *testing.T, events <-chan Event, eventType, phase string) {
t.Helper() t.Helper()
timer := time.NewTimer(10 * time.Second) timer := time.NewTimer(10 * time.Second)
defer timer.Stop() defer timer.Stop()
for { for {
select { select {
case event := <-events: case event := <-events:
if event.Type == "diagnostic" && event.Phase == phase { if event.Type == eventType && event.Phase == phase {
return return
} }
case <-timer.C: case <-timer.C:
t.Fatalf("did not receive %s diagnostic", phase) t.Fatalf("did not receive %s/%s event", eventType, phase)
} }
} }
} }
+5 -4
View File
@@ -15,6 +15,7 @@ import (
"time" "time"
"gamertan.com/sandwich-hime/internal/compiler" "gamertan.com/sandwich-hime/internal/compiler"
"gamertan.com/sandwich-hime/internal/testpath"
) )
type protocolClient struct { type protocolClient struct {
@@ -140,7 +141,7 @@ func (client *protocolClient) waitDiagnostics(t *testing.T, uri string, wantCode
} }
func TestServerOverlayFeaturesAndNoWrites(t *testing.T) { func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
root := t.TempDir() root := testpath.TempDir(t)
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n") writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n")
homePath := filepath.Join(root, "home.sando") homePath := filepath.Join(root, "home.sando")
badgePath := filepath.Join(root, "cards", "badge.sando") badgePath := filepath.Join(root, "cards", "badge.sando")
@@ -211,7 +212,7 @@ func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
} }
func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) { func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
root := t.TempDir() root := testpath.TempDir(t)
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n") writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n")
nestedPath := filepath.Join(root, "nested", "view.sando") nestedPath := filepath.Join(root, "nested", "view.sando")
writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n") writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n")
@@ -240,7 +241,7 @@ func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
} }
func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) { func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
root := t.TempDir() root := testpath.TempDir(t)
server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}} server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
@@ -263,7 +264,7 @@ func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
} }
func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) { func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) {
root := t.TempDir() root := testpath.TempDir(t)
server := &Server{ server := &Server{
root: root, root: root,
overlays: make(map[string]document, maxWorkspaceFiles+1), overlays: make(map[string]document, maxWorkspaceFiles+1),
+423
View File
@@ -0,0 +1,423 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package releaseartifact creates deterministic Hime-san release archives and
// checksummed native-platform verification receipts.
package releaseartifact
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
const receiptSchema = "himesan.native-verification.v1"
var (
releaseVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
gitObjectPattern = regexp.MustCompile(`^[0-9a-f]{40,64}$`)
digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
goVersionPattern = regexp.MustCompile(`^go(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
platformPattern = regexp.MustCompile(`^[a-z0-9]+$`)
)
// PackageOptions contains the reviewed inputs to one release archive.
type PackageOptions struct {
Version string
Commit string
Tree string
GoVersion string
GOOS string
GOARCH string
BinaryPath string
LicensePath string
ReleaseNotes string
OutputDirectory string
SourceDateEpoch int64
}
// PackageResult identifies the immutable unsigned release archive.
type PackageResult struct {
ArchivePath string `json:"archive_path"`
SHA256 string `json:"sha256"`
}
type buildInfo struct {
SchemaVersion int `json:"schema_version"`
Version string `json:"version"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GoVersion string `json:"go_version"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
BinarySHA256 string `json:"binary_sha256"`
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256,omitempty"`
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256,omitempty"`
SigningIdentifier string `json:"signing_identifier,omitempty"`
}
type spdxDocument struct {
SPDXVersion string `json:"spdxVersion"`
DataLicense string `json:"dataLicense"`
SPDXID string `json:"SPDXID"`
Name string `json:"name"`
DocumentNamespace string `json:"documentNamespace"`
CreationInfo creationInfo `json:"creationInfo"`
Packages []spdxPackage `json:"packages"`
}
type creationInfo struct {
Created string `json:"created"`
Creators []string `json:"creators"`
}
type spdxPackage struct {
Name string `json:"name"`
SPDXID string `json:"SPDXID"`
VersionInfo string `json:"versionInfo"`
DownloadLocation string `json:"downloadLocation"`
FilesAnalyzed bool `json:"filesAnalyzed"`
LicenseConcluded string `json:"licenseConcluded"`
LicenseDeclared string `json:"licenseDeclared"`
Checksums []spdxChecksum `json:"checksums"`
}
type spdxChecksum struct {
Algorithm string `json:"algorithm"`
ChecksumValue string `json:"checksumValue"`
}
// Receipt describes one completed native verification run.
type Receipt struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
GoVersion string `json:"go_version"`
RunnerVersion string `json:"runner_version"`
RunnerName string `json:"runner_name"`
GeneratedDigest string `json:"generated_output_sha256"`
UnsignedArtifactSHA string `json:"unsigned_artifact_sha256,omitempty"`
SuccessfulGates []string `json:"successful_gates"`
CompletedAt string `json:"completed_at"`
}
type archiveMember struct {
name string
mode int64
data []byte
}
// Package creates a byte-reproducible unsigned tar.gz and its SHA-256 sidecar.
func Package(options PackageOptions) (PackageResult, error) {
if err := validatePackageOptions(options); err != nil {
return PackageResult{}, err
}
binary, err := os.ReadFile(options.BinaryPath)
if err != nil {
return PackageResult{}, fmt.Errorf("read binary: %w", err)
}
license, err := os.ReadFile(options.LicensePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read license: %w", err)
}
releaseNotes, err := os.ReadFile(options.ReleaseNotes)
if err != nil {
return PackageResult{}, fmt.Errorf("read release notes: %w", err)
}
binaryDigest := digest(binary)
infoBytes, err := indentedJSON(buildInfo{
SchemaVersion: 1,
Version: options.Version,
Commit: options.Commit,
Tree: options.Tree,
GoVersion: options.GoVersion,
GOOS: options.GOOS,
GOARCH: options.GOARCH,
BinarySHA256: binaryDigest,
})
if err != nil {
return PackageResult{}, err
}
created := time.Unix(options.SourceDateEpoch, 0).UTC().Format(time.RFC3339)
sbomBytes, err := indentedJSON(spdxDocument{
SPDXVersion: "SPDX-2.3",
DataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
Name: "himesan-" + options.Version + "-" + options.GOOS + "-" + options.GOARCH,
DocumentNamespace: "https://sandwichhime.com/spdx/himesan/" + options.Version + "/" + options.Commit + "/" + options.GOOS + "-" + options.GOARCH,
CreationInfo: creationInfo{
Created: created,
Creators: []string{"Organization: Gamertan"},
},
Packages: []spdxPackage{{
Name: "himesan",
SPDXID: "SPDXRef-Package-himesan",
VersionInfo: options.Version,
DownloadLocation: "NOASSERTION",
FilesAnalyzed: false,
LicenseConcluded: "AGPL-3.0-only",
LicenseDeclared: "AGPL-3.0-only",
Checksums: []spdxChecksum{{
Algorithm: "SHA256",
ChecksumValue: binaryDigest,
}},
}},
})
if err != nil {
return PackageResult{}, err
}
members := []archiveMember{
{name: "BUILDINFO.json", mode: 0o444, data: infoBytes},
{name: "LICENSE", mode: 0o444, data: license},
{name: "RELEASE.md", mode: 0o444, data: releaseNotes},
{name: "SBOM.spdx.json", mode: 0o444, data: sbomBytes},
{name: "himesan", mode: 0o555, data: binary},
}
checksumLines := make([]string, 0, len(members))
for _, member := range members {
checksumLines = append(checksumLines, digest(member.data)+" "+member.name)
}
members = append(members, archiveMember{
name: "SHA256SUMS", mode: 0o444,
data: []byte(strings.Join(checksumLines, "\n") + "\n"),
})
sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name })
if err := os.MkdirAll(options.OutputDirectory, 0o755); err != nil {
return PackageResult{}, fmt.Errorf("create output directory: %w", err)
}
base := "himesan-" + strings.TrimPrefix(options.Version, "v") + "-" + options.GOOS + "-" + options.GOARCH
archivePath := filepath.Join(options.OutputDirectory, base+".tar.gz")
if err := writeArchive(archivePath, base, members, time.Unix(options.SourceDateEpoch, 0).UTC()); err != nil {
return PackageResult{}, err
}
archive, err := os.ReadFile(archivePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read completed archive: %w", err)
}
archiveDigest := digest(archive)
if err := writeExclusive(archivePath+".sha256", []byte(archiveDigest+" "+filepath.Base(archivePath)+"\n"), 0o444); err != nil {
return PackageResult{}, fmt.Errorf("write archive checksum: %w", err)
}
return PackageResult{ArchivePath: archivePath, SHA256: archiveDigest}, nil
}
// WriteReceipt validates, canonicalizes, and writes a receipt plus SHA sidecar.
func WriteReceipt(path string, receipt Receipt) (string, error) {
receipt.SchemaVersion = receiptSchema
if err := validateReceipt(receipt); err != nil {
return "", err
}
sort.Strings(receipt.SuccessfulGates)
contents, err := indentedJSON(receipt)
if err != nil {
return "", err
}
if err := writeExclusive(path, contents, 0o444); err != nil {
return "", fmt.Errorf("write receipt: %w", err)
}
checksum := digest(contents)
if err := writeExclusive(path+".sha256", []byte(checksum+" "+filepath.Base(path)+"\n"), 0o444); err != nil {
return "", fmt.Errorf("write receipt checksum: %w", err)
}
return checksum, nil
}
// DigestFiles returns a stable digest over sorted names and file contents.
func DigestFiles(paths []string) (string, error) {
if len(paths) == 0 {
return "", errors.New("at least one generated file is required")
}
sorted := append([]string(nil), paths...)
sort.Strings(sorted)
hash := sha256.New()
for _, path := range sorted {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s: %w", path, err)
}
_, _ = io.WriteString(hash, filepath.ToSlash(path))
_, _ = hash.Write([]byte{0})
_, _ = hash.Write(contents)
_, _ = hash.Write([]byte{0})
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func validatePackageOptions(options PackageOptions) error {
for name, value := range map[string]string{
"version": options.Version, "commit": options.Commit, "tree": options.Tree,
"go version": options.GoVersion, "GOOS": options.GOOS, "GOARCH": options.GOARCH,
"binary": options.BinaryPath, "license": options.LicensePath,
"release notes": options.ReleaseNotes, "output directory": options.OutputDirectory,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if options.SourceDateEpoch <= 0 {
return errors.New("source date epoch must be positive")
}
if !releaseVersionPattern.MatchString(options.Version) {
return errors.New("version must be a canonical v-prefixed semantic version without build metadata")
}
if !gitObjectPattern.MatchString(options.Commit) || !gitObjectPattern.MatchString(options.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(options.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(options.GOOS) || !platformPattern.MatchString(options.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
return nil
}
func validateReceipt(receipt Receipt) error {
for name, value := range map[string]string{
"repository": receipt.Repository, "commit": receipt.Commit, "tree": receipt.Tree,
"GOOS": receipt.GOOS, "GOARCH": receipt.GOARCH, "Go version": receipt.GoVersion,
"runner version": receipt.RunnerVersion, "runner name": receipt.RunnerName,
"generated digest": receipt.GeneratedDigest, "completed at": receipt.CompletedAt,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if len(receipt.SuccessfulGates) == 0 {
return errors.New("at least one successful gate is required")
}
if !gitObjectPattern.MatchString(receipt.Commit) || !gitObjectPattern.MatchString(receipt.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(receipt.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(receipt.GOOS) || !platformPattern.MatchString(receipt.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
if !digestPattern.MatchString(receipt.GeneratedDigest) {
return errors.New("generated-output digest must be a lowercase SHA-256")
}
if receipt.UnsignedArtifactSHA != "" && !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
return errors.New("unsigned-artifact digest must be a lowercase SHA-256")
}
if _, err := time.Parse(time.RFC3339, receipt.CompletedAt); err != nil {
return fmt.Errorf("completed at must be RFC3339: %w", err)
}
return nil
}
func writeArchive(path, root string, members []archiveMember, modified time.Time) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("create archive: %w", err)
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
gzipWriter := gzip.NewWriter(file)
gzipWriter.Header.ModTime = modified
gzipWriter.Header.OS = 255
tarWriter := tar.NewWriter(gzipWriter)
for _, member := range members {
header := &tar.Header{
Name: root + "/" + member.name,
Mode: member.mode,
Size: int64(len(member.data)),
ModTime: modified,
AccessTime: time.Time{},
ChangeTime: time.Time{},
Uid: 0,
Gid: 0,
Uname: "",
Gname: "",
Format: tar.FormatUSTAR,
}
if err := tarWriter.WriteHeader(header); err != nil {
return fmt.Errorf("write archive header: %w", err)
}
if _, err := tarWriter.Write(member.data); err != nil {
return fmt.Errorf("write archive member: %w", err)
}
}
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("close tar stream: %w", err)
}
if err := gzipWriter.Close(); err != nil {
return fmt.Errorf("close gzip stream: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("sync archive: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("close archive: %w", err)
}
if err := os.Chmod(path, 0o444); err != nil {
return fmt.Errorf("set archive permissions: %w", err)
}
success = true
return nil
}
func writeExclusive(path string, contents []byte, mode os.FileMode) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if err != nil {
return err
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
if _, err := file.Write(contents); err != nil {
return err
}
if err := file.Sync(); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
if err := os.Chmod(path, mode); err != nil {
return err
}
success = true
return nil
}
func indentedJSON(value any) ([]byte, error) {
contents, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode JSON: %w", err)
}
return append(contents, '\n'), nil
}
func digest(contents []byte) string {
sum := sha256.Sum256(contents)
return hex.EncodeToString(sum[:])
}
+103
View File
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestPackageIsByteReproducible(t *testing.T) {
directory := t.TempDir()
binary := writeFixture(t, directory, "himesan", "native binary")
license := writeFixture(t, directory, "LICENSE", "license")
releaseNotes := writeFixture(t, directory, "RELEASE.md", "release")
options := PackageOptions{
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40), GoVersion: "go1.27.0",
GOOS: "darwin", GOARCH: "arm64", BinaryPath: binary, LicensePath: license,
ReleaseNotes: releaseNotes, SourceDateEpoch: 1_700_000_000,
}
options.OutputDirectory = filepath.Join(directory, "first")
first, err := Package(options)
if err != nil {
t.Fatal(err)
}
options.OutputDirectory = filepath.Join(directory, "second")
second, err := Package(options)
if err != nil {
t.Fatal(err)
}
if first.SHA256 != second.SHA256 {
t.Fatalf("repeated packages differ: %s != %s", first.SHA256, second.SHA256)
}
}
func TestWriteReceiptSortsGatesAndWritesChecksum(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipt.json")
checksum, err := WriteReceipt(path, Receipt{
Repository: "example.test/development-source", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GOOS: "darwin", GOARCH: "arm64", GoVersion: "go1.27.0",
RunnerVersion: "v3.3.0", RunnerName: "mac", GeneratedDigest: strings.Repeat("c", 64),
SuccessfulGates: []string{"vet", "test"}, CompletedAt: time.Unix(1_700_000_000, 0).UTC().Format(time.RFC3339),
})
if err != nil {
t.Fatal(err)
}
if checksum == "" {
t.Fatal("empty checksum")
}
if _, err := os.Stat(path + ".sha256"); err != nil {
t.Fatal(err)
}
}
func TestPackageRejectsUnsafeVersionAndExistingArtifact(t *testing.T) {
directory := t.TempDir()
options := PackageOptions{
Version: "../../outside", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
BinaryPath: writeFixture(t, directory, "himesan", "native binary"),
LicensePath: writeFixture(t, directory, "LICENSE", "license"),
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
OutputDirectory: filepath.Join(directory, "output"), SourceDateEpoch: 1_700_000_000,
}
if _, err := Package(options); err == nil {
t.Fatal("unsafe version was accepted")
}
options.Version = "v1.0.0-rc.1"
if _, err := Package(options); err != nil {
t.Fatal(err)
}
if _, err := Package(options); err == nil {
t.Fatal("existing immutable artifact was overwritten")
}
}
func TestDigestFilesBindsNamesAndBytes(t *testing.T) {
directory := t.TempDir()
one := writeFixture(t, directory, "one", "same")
two := writeFixture(t, directory, "two", "same")
forward, err := DigestFiles([]string{two, one})
if err != nil {
t.Fatal(err)
}
reverse, err := DigestFiles([]string{one, two})
if err != nil {
t.Fatal(err)
}
if forward != reverse {
t.Fatal("file ordering changed digest")
}
}
func writeFixture(t *testing.T, directory, name, contents string) string {
t.Helper()
path := filepath.Join(directory, name)
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
}
+238
View File
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
"unicode/utf8"
)
const (
evidenceSchema = "himesan.release-evidence.v1"
evidenceManifestName = "RELEASE-EVIDENCE.json"
maximumEvidenceSize = 1 << 20
)
var requiredEvidenceFiles = []string{
"benchmark-methodology.md",
"development-supervisor.md",
"legal-review.md",
"native-platforms.md",
"security.md",
"signing-and-recovery.md",
"vanity-imports.md",
}
// EvidenceIdentity binds human-reviewed release evidence to one source state.
type EvidenceIdentity struct {
Repository string
Version string
Commit string
Tree string
ReviewedBy string
ReviewedAt string
}
type evidenceManifest struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Version string `json:"version"`
Commit string `json:"commit"`
Tree string `json:"tree"`
ReviewStatus string `json:"review_status"`
ReviewedBy string `json:"reviewed_by"`
ReviewedAt string `json:"reviewed_at"`
Files []evidenceFile `json:"files"`
}
type evidenceFile struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
}
// RequiredEvidenceFiles returns the fixed v1 human-review document set.
func RequiredEvidenceFiles() []string {
return append([]string(nil), requiredEvidenceFiles...)
}
// WriteEvidenceManifest seals the reviewed evidence documents without
// modifying them. The manifest is immutable and fails if it already exists.
func WriteEvidenceManifest(directory string, identity EvidenceIdentity) (string, error) {
if err := validateEvidenceIdentity(identity, true); err != nil {
return "", err
}
files, err := inspectEvidenceFiles(directory)
if err != nil {
return "", err
}
manifest := evidenceManifest{
SchemaVersion: evidenceSchema,
Repository: identity.Repository,
Version: identity.Version,
Commit: identity.Commit,
Tree: identity.Tree,
ReviewStatus: "reviewed",
ReviewedBy: identity.ReviewedBy,
ReviewedAt: identity.ReviewedAt,
Files: files,
}
contents, err := indentedJSON(manifest)
if err != nil {
return "", err
}
path := filepath.Join(directory, evidenceManifestName)
if err := writeExclusive(path, contents, 0o444); err != nil {
return "", fmt.Errorf("write release evidence manifest: %w", err)
}
return digest(contents), nil
}
// VerifyEvidenceManifest proves that the sealed evidence is for the expected
// source state and that every reviewed byte remains unchanged.
func VerifyEvidenceManifest(directory string, expected EvidenceIdentity) error {
if err := validateEvidenceIdentity(expected, false); err != nil {
return err
}
manifestPath := filepath.Join(directory, evidenceManifestName)
contents, err := readEvidenceFile(manifestPath, true)
if err != nil {
return fmt.Errorf("read release evidence manifest: %w", err)
}
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
var manifest evidenceManifest
if err := decoder.Decode(&manifest); err != nil {
return fmt.Errorf("decode release evidence manifest: %w", err)
}
if err := requireJSONEOF(decoder); err != nil {
return fmt.Errorf("decode release evidence manifest: %w", err)
}
if manifest.SchemaVersion != evidenceSchema {
return fmt.Errorf("release evidence schema = %q, want %q", manifest.SchemaVersion, evidenceSchema)
}
if manifest.ReviewStatus != "reviewed" {
return errors.New("release evidence review status must be reviewed")
}
actual := EvidenceIdentity{
Repository: manifest.Repository,
Version: manifest.Version,
Commit: manifest.Commit,
Tree: manifest.Tree,
ReviewedBy: manifest.ReviewedBy,
ReviewedAt: manifest.ReviewedAt,
}
if err := validateEvidenceIdentity(actual, true); err != nil {
return err
}
if actual.Repository != expected.Repository || actual.Version != expected.Version ||
actual.Commit != expected.Commit || actual.Tree != expected.Tree {
return errors.New("release evidence repository, version, commit, or tree does not match the candidate")
}
observed, err := inspectEvidenceFiles(directory)
if err != nil {
return err
}
if len(manifest.Files) != len(observed) {
return fmt.Errorf("release evidence manifest contains %d files, want %d", len(manifest.Files), len(observed))
}
for index := range observed {
if manifest.Files[index] != observed[index] {
return fmt.Errorf("release evidence file %q is missing, reordered, or has a changed digest", observed[index].Path)
}
}
return nil
}
func inspectEvidenceFiles(directory string) ([]evidenceFile, error) {
info, err := os.Lstat(directory)
if err != nil {
return nil, fmt.Errorf("inspect evidence directory: %w", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return nil, errors.New("evidence directory must be a real directory, not a symlink")
}
files := make([]evidenceFile, 0, len(requiredEvidenceFiles))
for _, name := range requiredEvidenceFiles {
contents, err := readEvidenceFile(filepath.Join(directory, name), false)
if err != nil {
return nil, fmt.Errorf("inspect %s: %w", name, err)
}
if !utf8.Valid(contents) || bytes.IndexByte(contents, 0) >= 0 {
return nil, fmt.Errorf("%s must be NUL-free UTF-8 text", name)
}
if !strings.Contains(string(contents), "# ") {
return nil, fmt.Errorf("%s must contain a Markdown heading", name)
}
files = append(files, evidenceFile{Path: name, SHA256: digest(contents)})
}
sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path })
return files, nil
}
func readEvidenceFile(path string, manifest bool) ([]byte, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return nil, errors.New("must be a regular file, not a symlink")
}
if info.Size() == 0 || info.Size() > maximumEvidenceSize {
return nil, fmt.Errorf("size must be between 1 and %d bytes", maximumEvidenceSize)
}
contents, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if manifest && len(contents) > maximumEvidenceSize {
return nil, errors.New("manifest exceeds the size limit")
}
return contents, nil
}
func validateEvidenceIdentity(identity EvidenceIdentity, requireReview bool) error {
if strings.TrimSpace(identity.Repository) == "" || strings.ContainsAny(identity.Repository, "\r\n\x00") {
return errors.New("evidence repository is required and must be one line")
}
if !releaseVersionPattern.MatchString(identity.Version) {
return errors.New("evidence version must be a canonical v-prefixed semantic version")
}
if !gitObjectPattern.MatchString(identity.Commit) || !gitObjectPattern.MatchString(identity.Tree) {
return errors.New("evidence commit and tree must be full lowercase Git object identities")
}
if !requireReview {
return nil
}
if strings.TrimSpace(identity.ReviewedBy) == "" || strings.ContainsAny(identity.ReviewedBy, "\r\n\x00") {
return errors.New("evidence reviewer is required and must be one line")
}
reviewedAt, err := time.Parse(time.RFC3339, identity.ReviewedAt)
if err != nil {
return fmt.Errorf("evidence review time must be RFC3339: %w", err)
}
if reviewedAt.After(time.Now().UTC().Add(5 * time.Minute)) {
return errors.New("evidence review time cannot be in the future")
}
return nil
}
func requireJSONEOF(decoder *json.Decoder) error {
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("multiple JSON values are not allowed")
}
return err
}
return nil
}
+123
View File
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEvidenceManifestBindsReviewedContentAndIdentity(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
checksum, err := WriteEvidenceManifest(directory, identity)
if err != nil {
t.Fatal(err)
}
if !digestPattern.MatchString(checksum) {
t.Fatalf("manifest checksum = %q", checksum)
}
if err := VerifyEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, requiredEvidenceFiles[0])
if err := os.WriteFile(path, []byte("# Changed after review\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("changed evidence bytes were accepted")
}
}
func TestEvidenceManifestRejectsCandidateMismatchAndUnknownFields(t *testing.T) {
t.Run("candidate identity", func(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
identity.Commit = strings.Repeat("d", 40)
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("mismatched candidate was accepted")
}
})
t.Run("unknown manifest field", func(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, evidenceManifestName)
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
contents = []byte(strings.Replace(string(contents), `"files":`, `"unexpected": true, "files":`, 1))
if err := os.Chmod(path, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("unknown manifest field was accepted")
}
})
}
func TestEvidenceManifestRejectsSymlinkAndPlaceholderDocuments(t *testing.T) {
t.Run("symlink", func(t *testing.T) {
directory := evidenceFixture(t)
name := requiredEvidenceFiles[0]
path := filepath.Join(directory, name)
target := filepath.Join(directory, "target.md")
if err := os.WriteFile(target, []byte("# Target\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, path); err != nil {
t.Fatal(err)
}
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
t.Fatal("symlinked evidence was accepted")
}
})
t.Run("placeholder", func(t *testing.T) {
directory := evidenceFixture(t)
if err := os.WriteFile(filepath.Join(directory, requiredEvidenceFiles[0]), []byte("not reviewed\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
t.Fatal("heading-free placeholder was accepted")
}
})
}
func evidenceFixture(t *testing.T) string {
t.Helper()
directory := t.TempDir()
for _, name := range requiredEvidenceFiles {
if err := os.WriteFile(filepath.Join(directory, name), []byte("# Reviewed "+name+"\n\nExact bounded evidence.\n"), 0o600); err != nil {
t.Fatal(err)
}
}
return directory
}
func evidenceIdentity() EvidenceIdentity {
return EvidenceIdentity{
Repository: "gamertan/sandwich-hime",
Version: "v1.0.0-rc.1",
Commit: strings.Repeat("a", 40),
Tree: strings.Repeat("b", 40),
ReviewedBy: "release operator",
ReviewedAt: "2026-08-23T18:00:00Z",
}
}
+364
View File
@@ -0,0 +1,364 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
const maximumNativeArchiveSize = 256 << 20
var unsignedPackageMembers = map[string]os.FileMode{
"BUILDINFO.json": 0o444,
"LICENSE": 0o444,
"RELEASE.md": 0o444,
"SBOM.spdx.json": 0o444,
"SHA256SUMS": 0o444,
"himesan": 0o555,
}
// MacOSSigningOptions are the reviewed identities added after Developer ID
// signing changes the native binary bytes.
type MacOSSigningOptions struct {
Directory string
UnsignedArchiveSHA256 string
Identity string
Identifier string
FinalizedAt string
}
type signingInfo struct {
SchemaVersion int `json:"schema_version"`
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256"`
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256"`
SignedBinarySHA256 string `json:"signed_binary_sha256"`
Identity string `json:"identity"`
Identifier string `json:"identifier"`
FinalizedAt string `json:"finalized_at"`
}
// ExtractVerifiedMacOSPackage verifies the approved unsigned archive and
// extracts its fixed file set without delegating path handling to system tar.
func ExtractVerifiedMacOSPackage(archivePath, expectedSHA256, outputDirectory string) (string, error) {
if !digestPattern.MatchString(expectedSHA256) {
return "", errors.New("approved archive digest must be a lowercase SHA-256")
}
info, err := os.Lstat(archivePath)
if err != nil {
return "", fmt.Errorf("inspect unsigned archive: %w", err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return "", errors.New("unsigned archive must be a regular file, not a symlink")
}
if info.Size() <= 0 || info.Size() > maximumNativeArchiveSize {
return "", errors.New("unsigned archive size is outside the permitted range")
}
archive, err := os.ReadFile(archivePath)
if err != nil {
return "", fmt.Errorf("read unsigned archive: %w", err)
}
if digest(archive) != expectedSHA256 {
return "", errors.New("unsigned archive does not match the approved digest")
}
members, root, err := readUnsignedPackage(archive)
if err != nil {
return "", err
}
if err := validateUnsignedPackage(members, root); err != nil {
return "", err
}
outputInfo, err := os.Lstat(outputDirectory)
if err != nil {
return "", fmt.Errorf("inspect extraction directory: %w", err)
}
if !outputInfo.IsDir() || outputInfo.Mode()&os.ModeSymlink != 0 {
return "", errors.New("extraction destination must be a real directory")
}
rootPath := filepath.Join(outputDirectory, root)
if err := os.Mkdir(rootPath, 0o700); err != nil {
return "", fmt.Errorf("create extracted package root: %w", err)
}
success := false
defer func() {
if !success {
_ = os.RemoveAll(rootPath)
}
}()
for name, mode := range unsignedPackageMembers {
if err := writeExclusive(filepath.Join(rootPath, name), members[name], mode); err != nil {
return "", fmt.Errorf("extract %s: %w", name, err)
}
}
success = true
return rootPath, nil
}
// FinalizeSignedMacOSDistribution replaces unsigned binary provenance with a
// complete signed-distribution record and regenerates every internal checksum.
func FinalizeSignedMacOSDistribution(options MacOSSigningOptions) error {
if !digestPattern.MatchString(options.UnsignedArchiveSHA256) {
return errors.New("unsigned archive digest must be a lowercase SHA-256")
}
if strings.TrimSpace(options.Identity) == "" || strings.ContainsAny(options.Identity, "\r\n\x00") {
return errors.New("signing identity is required and must be one line")
}
if strings.TrimSpace(options.Identifier) == "" || strings.ContainsAny(options.Identifier, "\r\n\x00") {
return errors.New("signing identifier is required and must be one line")
}
if _, err := time.Parse(time.RFC3339, options.FinalizedAt); err != nil {
return fmt.Errorf("finalization time must be RFC3339: %w", err)
}
for name := range unsignedPackageMembers {
if _, err := readRegularFile(filepath.Join(options.Directory, name), maximumNativeArchiveSize); err != nil {
return fmt.Errorf("inspect signed distribution %s: %w", name, err)
}
}
buildPath := filepath.Join(options.Directory, "BUILDINFO.json")
buildContents, err := os.ReadFile(buildPath)
if err != nil {
return fmt.Errorf("read unsigned build information: %w", err)
}
var build buildInfo
if err := decodeStrictJSON(buildContents, &build); err != nil {
return fmt.Errorf("decode unsigned build information: %w", err)
}
if build.SchemaVersion != 1 || !digestPattern.MatchString(build.BinarySHA256) || build.GOOS != "darwin" || build.GOARCH != "arm64" {
return errors.New("unsigned build information is not a supported Darwin/arm64 package")
}
unsignedBinaryDigest := build.BinarySHA256
signedBinary, err := os.ReadFile(filepath.Join(options.Directory, "himesan"))
if err != nil {
return fmt.Errorf("read signed binary: %w", err)
}
signedBinaryDigest := digest(signedBinary)
if signedBinaryDigest == unsignedBinaryDigest {
return errors.New("Developer ID signing did not change the native binary bytes")
}
build.SchemaVersion = 2
build.BinarySHA256 = signedBinaryDigest
build.UnsignedBinarySHA256 = unsignedBinaryDigest
build.UnsignedArchiveSHA256 = options.UnsignedArchiveSHA256
build.SigningIdentifier = options.Identifier
newBuild, err := indentedJSON(build)
if err != nil {
return err
}
sbomPath := filepath.Join(options.Directory, "SBOM.spdx.json")
sbomContents, err := os.ReadFile(sbomPath)
if err != nil {
return fmt.Errorf("read unsigned SBOM: %w", err)
}
var sbom spdxDocument
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
return fmt.Errorf("decode unsigned SBOM: %w", err)
}
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
sbom.Packages[0].Checksums[0].ChecksumValue != unsignedBinaryDigest {
return errors.New("unsigned SBOM does not match the unsigned binary")
}
sbom.Packages[0].Checksums[0].ChecksumValue = signedBinaryDigest
newSBOM, err := indentedJSON(sbom)
if err != nil {
return err
}
signingContents, err := indentedJSON(signingInfo{
SchemaVersion: 1,
UnsignedArchiveSHA256: options.UnsignedArchiveSHA256,
UnsignedBinarySHA256: unsignedBinaryDigest,
SignedBinarySHA256: signedBinaryDigest,
Identity: options.Identity,
Identifier: options.Identifier,
FinalizedAt: options.FinalizedAt,
})
if err != nil {
return err
}
if err := replaceRegularFile(buildPath, newBuild, 0o444); err != nil {
return fmt.Errorf("replace signed build information: %w", err)
}
if err := replaceRegularFile(sbomPath, newSBOM, 0o444); err != nil {
return fmt.Errorf("replace signed SBOM: %w", err)
}
if err := writeExclusive(filepath.Join(options.Directory, "SIGNING.json"), signingContents, 0o444); err != nil {
return fmt.Errorf("write signing information: %w", err)
}
checksumNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"}
var checksumLines []string
for _, name := range checksumNames {
contents, err := os.ReadFile(filepath.Join(options.Directory, name))
if err != nil {
return fmt.Errorf("read signed distribution member %s: %w", name, err)
}
checksumLines = append(checksumLines, digest(contents)+" "+name)
}
checksums := []byte(strings.Join(checksumLines, "\n") + "\n")
if err := replaceRegularFile(filepath.Join(options.Directory, "SHA256SUMS"), checksums, 0o444); err != nil {
return fmt.Errorf("replace signed distribution checksums: %w", err)
}
return nil
}
func readUnsignedPackage(archive []byte) (map[string][]byte, string, error) {
gzipReader, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
return nil, "", fmt.Errorf("open unsigned gzip archive: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(io.LimitReader(gzipReader, maximumNativeArchiveSize+1))
members := make(map[string][]byte, len(unsignedPackageMembers))
root := ""
var totalSize int64
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, "", fmt.Errorf("read unsigned tar archive: %w", err)
}
if header.Typeflag != tar.TypeReg || header.Size < 0 || header.Size > maximumNativeArchiveSize {
return nil, "", errors.New("unsigned archive contains a non-regular or oversized member")
}
totalSize += header.Size
if totalSize > maximumNativeArchiveSize {
return nil, "", errors.New("unsigned archive expands beyond the permitted size")
}
clean := filepath.ToSlash(filepath.Clean(header.Name))
parts := strings.Split(clean, "/")
if len(parts) != 2 || parts[0] == "" || parts[0] == "." || parts[0] == ".." {
return nil, "", fmt.Errorf("unsigned archive member has an unsafe path: %q", header.Name)
}
if root == "" {
root = parts[0]
} else if root != parts[0] {
return nil, "", errors.New("unsigned archive contains more than one package root")
}
name := parts[1]
expectedMode, ok := unsignedPackageMembers[name]
if !ok || os.FileMode(header.Mode).Perm() != expectedMode || header.Linkname != "" {
return nil, "", fmt.Errorf("unsigned archive member %q has an unexpected name, mode, or link", name)
}
if _, exists := members[name]; exists {
return nil, "", fmt.Errorf("unsigned archive repeats member %q", name)
}
contents, err := io.ReadAll(io.LimitReader(tarReader, header.Size+1))
if err != nil || int64(len(contents)) != header.Size {
return nil, "", fmt.Errorf("read unsigned archive member %q", name)
}
members[name] = contents
}
if err := gzipReader.Close(); err != nil {
return nil, "", fmt.Errorf("finish unsigned gzip archive: %w", err)
}
return members, root, nil
}
func validateUnsignedPackage(members map[string][]byte, root string) error {
if len(members) != len(unsignedPackageMembers) || root == "" {
return errors.New("unsigned archive does not contain the exact release file set")
}
var build buildInfo
if err := decodeStrictJSON(members["BUILDINFO.json"], &build); err != nil {
return fmt.Errorf("decode unsigned build information: %w", err)
}
if build.SchemaVersion != 1 || build.GOOS != "darwin" || build.GOARCH != "arm64" ||
!releaseVersionPattern.MatchString(build.Version) || root != "himesan-"+strings.TrimPrefix(build.Version, "v")+"-darwin-arm64" ||
!gitObjectPattern.MatchString(build.Commit) || !gitObjectPattern.MatchString(build.Tree) ||
!goVersionPattern.MatchString(build.GoVersion) || build.BinarySHA256 != digest(members["himesan"]) {
return errors.New("unsigned build information does not match the archive")
}
var sbom spdxDocument
if err := decodeStrictJSON(members["SBOM.spdx.json"], &sbom); err != nil {
return fmt.Errorf("decode unsigned SBOM: %w", err)
}
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
sbom.Packages[0].Checksums[0].ChecksumValue != build.BinarySHA256 {
return errors.New("unsigned SBOM does not match the native binary")
}
expectedNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "himesan"}
var expectedLines []string
for _, name := range expectedNames {
expectedLines = append(expectedLines, digest(members[name])+" "+name)
}
if string(members["SHA256SUMS"]) != strings.Join(expectedLines, "\n")+"\n" {
return errors.New("unsigned package checksum manifest does not match its members")
}
return nil
}
func readRegularFile(path string, maximum int64) ([]byte, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() < 0 || info.Size() > maximum {
return nil, errors.New("must be a bounded regular file, not a symlink")
}
return os.ReadFile(path)
}
func decodeStrictJSON(contents []byte, target any) error {
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
return requireJSONEOF(decoder)
}
func replaceRegularFile(path string, contents []byte, mode os.FileMode) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("replacement target must be a regular file, not a symlink")
}
temporary, err := os.CreateTemp(filepath.Dir(path), ".himesan-release-*")
if err != nil {
return err
}
temporaryPath := temporary.Name()
success := false
defer func() {
_ = temporary.Close()
if !success {
_ = os.Remove(temporaryPath)
}
}()
if _, err := temporary.Write(contents); err != nil {
return err
}
if err := temporary.Sync(); err != nil {
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Chmod(temporaryPath, mode); err != nil {
return err
}
if err := os.Rename(temporaryPath, path); err != nil {
return err
}
success = true
return nil
}
+181
View File
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"os"
"path/filepath"
"strings"
"testing"
)
func TestExtractAndFinalizeSignedMacOSDistribution(t *testing.T) {
archive, archiveDigest := macOSPackageFixture(t)
extraction := t.TempDir()
root, err := ExtractVerifiedMacOSPackage(archive, archiveDigest, extraction)
if err != nil {
t.Fatal(err)
}
binaryPath := filepath.Join(root, "himesan")
unsignedBinary, err := os.ReadFile(binaryPath)
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(binaryPath, 0o700); err != nil {
t.Fatal(err)
}
binary, err := os.OpenFile(binaryPath, os.O_APPEND|os.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
if _, err := binary.WriteString("developer-id-signature"); err != nil {
t.Fatal(err)
}
if err := binary.Close(); err != nil {
t.Fatal(err)
}
options := MacOSSigningOptions{
Directory: root, UnsignedArchiveSHA256: archiveDigest,
Identity: "Developer ID Application: Example (TEAMID)",
Identifier: "com.gamertan.sandwich-hime.himesan",
FinalizedAt: "2026-08-23T18:00:00Z",
}
if err := FinalizeSignedMacOSDistribution(options); err != nil {
t.Fatal(err)
}
var build buildInfo
buildContents, err := os.ReadFile(filepath.Join(root, "BUILDINFO.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(buildContents, &build); err != nil {
t.Fatal(err)
}
signedBinary, err := os.ReadFile(binaryPath)
if err != nil {
t.Fatal(err)
}
if build.SchemaVersion != 2 || build.BinarySHA256 != digest(signedBinary) ||
build.UnsignedBinarySHA256 != digest(unsignedBinary) || build.UnsignedArchiveSHA256 != archiveDigest ||
build.SigningIdentifier != options.Identifier {
t.Fatalf("signed build information is incomplete: %#v", build)
}
var signing signingInfo
signingContents, err := os.ReadFile(filepath.Join(root, "SIGNING.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(signingContents, &signing); err != nil {
t.Fatal(err)
}
if signing.SignedBinarySHA256 != build.BinarySHA256 || signing.UnsignedBinarySHA256 != build.UnsignedBinarySHA256 {
t.Fatalf("signing record does not match build information: %#v", signing)
}
var sbom spdxDocument
sbomContents, err := os.ReadFile(filepath.Join(root, "SBOM.spdx.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
t.Fatal(err)
}
if got := sbom.Packages[0].Checksums[0].ChecksumValue; got != build.BinarySHA256 {
t.Fatalf("signed SBOM checksum = %s, want %s", got, build.BinarySHA256)
}
assertChecksumManifest(t, root, []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"})
if err := FinalizeSignedMacOSDistribution(options); err == nil {
t.Fatal("signed distribution was finalized twice")
}
}
func TestExtractVerifiedMacOSPackageRejectsSubstitutionAndSymlink(t *testing.T) {
archive, archiveDigest := macOSPackageFixture(t)
if _, err := ExtractVerifiedMacOSPackage(archive, strings.Repeat("0", 64), t.TempDir()); err == nil {
t.Fatal("archive substitution was accepted")
}
symlink := filepath.Join(t.TempDir(), "candidate.tar.gz")
if err := os.Symlink(archive, symlink); err != nil {
t.Fatal(err)
}
if _, err := ExtractVerifiedMacOSPackage(symlink, archiveDigest, t.TempDir()); err == nil {
t.Fatal("symlinked archive was accepted")
}
unsafeArchive := filepath.Join(t.TempDir(), "unsafe.tar.gz")
destination := t.TempDir()
escapeName := filepath.Base(destination) + "-outside"
unsafeContents := unsafeTarGzip(t, "../"+escapeName, []byte("not a package"))
if err := os.WriteFile(unsafeArchive, unsafeContents, 0o600); err != nil {
t.Fatal(err)
}
if _, err := ExtractVerifiedMacOSPackage(unsafeArchive, digest(unsafeContents), destination); err == nil {
t.Fatal("archive path traversal was accepted")
}
if _, err := os.Stat(filepath.Join(filepath.Dir(destination), escapeName)); !os.IsNotExist(err) {
t.Fatal("unsafe archive wrote outside the extraction directory")
}
}
func macOSPackageFixture(t *testing.T) (string, string) {
t.Helper()
directory := t.TempDir()
options := PackageOptions{
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
BinaryPath: writeFixture(t, directory, "himesan", "unsigned Mach-O fixture"),
LicensePath: writeFixture(t, directory, "LICENSE", "licence"),
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
OutputDirectory: filepath.Join(directory, "package"), SourceDateEpoch: 1_700_000_000,
}
result, err := Package(options)
if err != nil {
t.Fatal(err)
}
return result.ArchivePath, result.SHA256
}
func assertChecksumManifest(t *testing.T, directory string, names []string) {
t.Helper()
var lines []string
for _, name := range names {
contents, err := os.ReadFile(filepath.Join(directory, name))
if err != nil {
t.Fatal(err)
}
lines = append(lines, digest(contents)+" "+name)
}
checksums, err := os.ReadFile(filepath.Join(directory, "SHA256SUMS"))
if err != nil {
t.Fatal(err)
}
if got, want := string(checksums), strings.Join(lines, "\n")+"\n"; got != want {
t.Fatalf("checksum manifest mismatch\ngot:\n%s\nwant:\n%s", got, want)
}
}
func unsafeTarGzip(t *testing.T, name string, contents []byte) []byte {
t.Helper()
var output bytes.Buffer
gzipWriter := gzip.NewWriter(&output)
tarWriter := tar.NewWriter(gzipWriter)
if err := tarWriter.WriteHeader(&tar.Header{Name: name, Mode: 0o444, Size: int64(len(contents)), Typeflag: tar.TypeReg}); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write(contents); err != nil {
t.Fatal(err)
}
if err := tarWriter.Close(); err != nil {
t.Fatal(err)
}
if err := gzipWriter.Close(); err != nil {
t.Fatal(err)
}
return output.Bytes()
}
+220
View File
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
const nativeReceiptMaximumAge = 30 * 24 * time.Hour
var runnerVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
type nativeLane struct {
directory string
goos string
goarch string
goVersion string
runner string
}
var requiredNativeLanes = []nativeLane{
{directory: "darwin-arm64-go1.26.7", goos: "darwin", goarch: "arm64", goVersion: "go1.26.7", runner: "macbook-air-himesan-darwin-arm64"},
{directory: "darwin-arm64-go1.27.0", goos: "darwin", goarch: "arm64", goVersion: "go1.27.0", runner: "macbook-air-himesan-darwin-arm64"},
{directory: "linux-amd64-go1.26.7", goos: "linux", goarch: "amd64", goVersion: "go1.26.7", runner: "cliff-himesan-linux-amd64"},
{directory: "linux-amd64-go1.27.0", goos: "linux", goarch: "amd64", goVersion: "go1.27.0", runner: "cliff-himesan-linux-amd64"},
}
var requiredNativeGates = []string{
"build", "consumer", "contracts", "fuzz", "generation", "package",
"public-snapshot", "race", "test", "vet", "vulnerability",
}
// NativeReceiptExpectation identifies the exact source verified by all native
// runner lanes.
type NativeReceiptExpectation struct {
Repository string
Commit string
Tree string
}
// NativeReceiptSummary is safe to include in human release evidence.
type NativeReceiptSummary struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GeneratedDigest string `json:"generated_output_sha256"`
Lanes []NativeLaneSummary `json:"lanes"`
}
// NativeLaneSummary records one verified native receipt without local paths.
type NativeLaneSummary struct {
Platform string `json:"platform"`
GoVersion string `json:"go_version"`
RunnerVersion string `json:"runner_version"`
ArtifactSHA256 string `json:"unsigned_artifact_sha256"`
ReceiptSHA256 string `json:"receipt_sha256"`
CompletedAt string `json:"completed_at"`
}
// VerifyNativeReceiptSet requires all four maintained target/toolchain lanes,
// validates their checksum sidecars, and proves identical generated output.
func VerifyNativeReceiptSet(directory string, expected NativeReceiptExpectation) (NativeReceiptSummary, error) {
if strings.TrimSpace(expected.Repository) == "" || strings.ContainsAny(expected.Repository, "\r\n\x00") {
return NativeReceiptSummary{}, errors.New("native receipt repository is required and must be one line")
}
if !gitObjectPattern.MatchString(expected.Commit) || !gitObjectPattern.MatchString(expected.Tree) {
return NativeReceiptSummary{}, errors.New("native receipt commit and tree must be full lowercase Git object identities")
}
if err := validateNativeDirectory(directory); err != nil {
return NativeReceiptSummary{}, err
}
summary := NativeReceiptSummary{
SchemaVersion: "himesan.native-receipt-set.v1",
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
}
for _, lane := range requiredNativeLanes {
receipt, receiptDigest, err := readNativeLane(filepath.Join(directory, lane.directory))
if err != nil {
return NativeReceiptSummary{}, fmt.Errorf("verify native lane %s: %w", lane.directory, err)
}
if receipt.SchemaVersion != receiptSchema || receipt.Repository != expected.Repository ||
receipt.Commit != expected.Commit || receipt.Tree != expected.Tree ||
receipt.GOOS != lane.goos || receipt.GOARCH != lane.goarch ||
receipt.GoVersion != lane.goVersion || receipt.RunnerName != lane.runner {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s does not match its source, platform, toolchain, or runner", lane.directory)
}
if !runnerVersionPattern.MatchString(receipt.RunnerVersion) || !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s has an invalid runner or artifact identity", lane.directory)
}
completedAt, err := time.Parse(time.RFC3339, receipt.CompletedAt)
if err != nil || completedAt.After(time.Now().UTC().Add(5*time.Minute)) || time.Since(completedAt) > nativeReceiptMaximumAge {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s is not fresh, completed, RFC3339 evidence", lane.directory)
}
if err := requireNativeGates(receipt.SuccessfulGates); err != nil {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s: %w", lane.directory, err)
}
if summary.GeneratedDigest == "" {
summary.GeneratedDigest = receipt.GeneratedDigest
} else if summary.GeneratedDigest != receipt.GeneratedDigest {
return NativeReceiptSummary{}, errors.New("native lanes did not produce identical generated output")
}
summary.Lanes = append(summary.Lanes, NativeLaneSummary{
Platform: lane.goos + "/" + lane.goarch, GoVersion: lane.goVersion,
RunnerVersion: receipt.RunnerVersion, ArtifactSHA256: receipt.UnsignedArtifactSHA,
ReceiptSHA256: receiptDigest, CompletedAt: receipt.CompletedAt,
})
}
return summary, nil
}
func validateNativeDirectory(directory string) error {
info, err := os.Lstat(directory)
if err != nil {
return fmt.Errorf("inspect native evidence directory: %w", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("native evidence must be a real directory")
}
entries, err := os.ReadDir(directory)
if err != nil {
return fmt.Errorf("read native evidence directory: %w", err)
}
expected := make([]string, 0, len(requiredNativeLanes))
for _, lane := range requiredNativeLanes {
expected = append(expected, lane.directory)
}
observed := make([]string, 0, len(entries))
for _, entry := range entries {
observed = append(observed, entry.Name())
}
sort.Strings(expected)
sort.Strings(observed)
if !equalStrings(expected, observed) {
return fmt.Errorf("native evidence directories = %v, want exactly %v", observed, expected)
}
return nil
}
func readNativeLane(directory string) (Receipt, string, error) {
info, err := os.Lstat(directory)
if err != nil {
return Receipt{}, "", err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return Receipt{}, "", errors.New("lane must be a real directory")
}
entries, err := os.ReadDir(directory)
if err != nil {
return Receipt{}, "", err
}
if len(entries) != 2 || entries[0].Name() != "TEND-CI-VERIFICATION.json" || entries[1].Name() != "TEND-CI-VERIFICATION.json.sha256" {
return Receipt{}, "", errors.New("lane must contain only its receipt and checksum sidecar")
}
receiptPath := filepath.Join(directory, "TEND-CI-VERIFICATION.json")
contents, err := readRegularFile(receiptPath, maximumEvidenceSize)
if err != nil || len(contents) == 0 {
return Receipt{}, "", errors.New("receipt must be a non-empty bounded regular file")
}
receiptDigest := digest(contents)
sidecar, err := readRegularFile(receiptPath+".sha256", 512)
if err != nil {
return Receipt{}, "", fmt.Errorf("read receipt checksum: %w", err)
}
expectedSidecar := receiptDigest + " TEND-CI-VERIFICATION.json\n"
if string(sidecar) != expectedSidecar {
return Receipt{}, "", errors.New("receipt checksum sidecar does not match")
}
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
var receipt Receipt
if err := decoder.Decode(&receipt); err != nil {
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
}
if err := requireJSONEOF(decoder); err != nil {
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
}
if err := validateReceipt(receipt); err != nil {
return Receipt{}, "", err
}
return receipt, receiptDigest, nil
}
func requireNativeGates(gates []string) error {
observed := make(map[string]bool, len(gates))
for _, gate := range gates {
if observed[gate] {
return fmt.Errorf("successful gate %q is duplicated", gate)
}
observed[gate] = true
}
for _, gate := range requiredNativeGates {
if !observed[gate] {
return fmt.Errorf("required successful gate %q is missing", gate)
}
}
return nil
}
func equalStrings(left, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestVerifyNativeReceiptSetRequiresEveryLaneAndMatchingGeneratedOutput(t *testing.T) {
expected := NativeReceiptExpectation{
Repository: "gamertan/sandwich-hime",
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
}
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
summary, err := VerifyNativeReceiptSet(directory, expected)
if err != nil {
t.Fatal(err)
}
if summary.GeneratedDigest != strings.Repeat("c", 64) || len(summary.Lanes) != 4 {
t.Fatalf("native summary is incomplete: %#v", summary)
}
t.Run("source mismatch", func(t *testing.T) {
mismatch := expected
mismatch.Commit = strings.Repeat("d", 40)
if _, err := VerifyNativeReceiptSet(directory, mismatch); err == nil {
t.Fatal("mismatched source commit was accepted")
}
})
t.Run("generated mismatch", func(t *testing.T) {
mismatched := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
lane := requiredNativeLanes[0]
laneDirectory := filepath.Join(mismatched, lane.directory)
if err := os.RemoveAll(laneDirectory); err != nil {
t.Fatal(err)
}
writeNativeLaneFixture(t, laneDirectory, lane, expected, strings.Repeat("e", 64))
if _, err := VerifyNativeReceiptSet(mismatched, expected); err == nil {
t.Fatal("different generated output was accepted")
}
})
}
func TestVerifyNativeReceiptSetRejectsTamperingAndUnexpectedFiles(t *testing.T) {
expected := NativeReceiptExpectation{
Repository: "gamertan/sandwich-hime",
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
}
t.Run("checksum", func(t *testing.T) {
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
sidecar := filepath.Join(directory, requiredNativeLanes[0].directory, "TEND-CI-VERIFICATION.json.sha256")
if err := os.Chmod(sidecar, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(sidecar, []byte(strings.Repeat("0", 64)+" TEND-CI-VERIFICATION.json\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
t.Fatal("tampered checksum was accepted")
}
})
t.Run("unexpected entry", func(t *testing.T) {
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
if err := os.WriteFile(filepath.Join(directory, "notes.txt"), []byte("not a receipt"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
t.Fatal("unexpected native evidence entry was accepted")
}
})
}
func nativeReceiptFixture(t *testing.T, expected NativeReceiptExpectation, generatedDigest string) string {
t.Helper()
directory := t.TempDir()
for _, lane := range requiredNativeLanes {
writeNativeLaneFixture(t, filepath.Join(directory, lane.directory), lane, expected, generatedDigest)
}
return directory
}
func writeNativeLaneFixture(t *testing.T, directory string, lane nativeLane, expected NativeReceiptExpectation, generatedDigest string) {
t.Helper()
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
runnerVersion := "v3.1.0"
if lane.goos == "darwin" {
runnerVersion = "v3.3.0"
}
receipt := Receipt{
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
GOOS: lane.goos, GOARCH: lane.goarch, GoVersion: lane.goVersion,
RunnerVersion: runnerVersion, RunnerName: lane.runner,
GeneratedDigest: generatedDigest, UnsignedArtifactSHA: strings.Repeat("f", 64),
SuccessfulGates: append([]string(nil), requiredNativeGates...),
CompletedAt: time.Now().UTC().Add(-time.Minute).Format(time.RFC3339),
}
if _, err := WriteReceipt(filepath.Join(directory, "TEND-CI-VERIFICATION.json"), receipt); err != nil {
t.Fatal(err)
}
}
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package testpath provides filesystem helpers for tests that exercise
// Hime-san's deliberate symlink boundaries.
package testpath
import (
"os"
"path/filepath"
"testing"
)
// TempDir returns the physical path to a fresh directory owned by the test.
//
// macOS commonly exposes its temporary directory through /var even though
// /var is a root-owned system symlink to /private/var. Resolving a directory
// immediately after testing.TB creates it keeps tests portable without
// teaching production path validation to follow user-controlled symlinks.
func TempDir(t testing.TB) string {
t.Helper()
directory := t.TempDir()
resolved, err := filepath.EvalSymlinks(directory)
if err != nil {
t.Fatalf("resolve test temporary directory: %v", err)
}
resolved = filepath.Clean(resolved)
info, err := os.Lstat(resolved)
if err != nil {
t.Fatalf("inspect resolved test temporary directory: %v", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
t.Fatalf("resolved test temporary directory is not a physical directory: %s", resolved)
}
return resolved
}
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
package testpath
import (
"path/filepath"
"testing"
)
func TestTempDirReturnsPhysicalPath(t *testing.T) {
directory := TempDir(t)
resolved, err := filepath.EvalSymlinks(directory)
if err != nil {
t.Fatal(err)
}
if filepath.Clean(resolved) != directory {
t.Fatalf("TempDir() = %q, physical path = %q", directory, resolved)
}
}
+3
View File
@@ -16,4 +16,7 @@ type ComponentFunc func(context.Context, io.Writer) error
`Render` invokes components; generated code uses the context-specific writer helpers and checks `ABI`. Opaque `TrustedHTML`, `TrustedURL`, `TrustedJS`, and `TrustedCSS` values can be constructed only by conspicuous `Trust*` calls in trusted application code. `Render` invokes components; generated code uses the context-specific writer helpers and checks `ABI`. Opaque `TrustedHTML`, `TrustedURL`, `TrustedJS`, and `TrustedCSS` values can be constructed only by conspicuous `Trust*` calls in trusted application code.
The exact exported v1 symbol, value, method, field, and signature inventory is
machine-checked in [`testdata/public-api-v1.txt`](testdata/public-api-v1.txt).
The runtime has its own `go.mod`, Apache-2.0 license, `COPYRIGHT` record, semantic version, and `sando/vX.Y.Z` tags. It never imports the AGPL compiler. Importing the runtime does not make an application AGPL under the project's terms. Applications may license their own code and generated files under terms they choose to the extent they hold the necessary rights, while redistribution of the runtime remains subject to Apache-2.0. The runtime has its own `go.mod`, Apache-2.0 license, `COPYRIGHT` record, semantic version, and `sando/vX.Y.Z` tags. It never imports the AGPL compiler. Importing the runtime does not make an application AGPL under the project's terms. Applications may license their own code and generated files under terms they choose to the extent they hold the necessary rights, while redistribution of the runtime remains subject to Apache-2.0.
+106
View File
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0
package sando
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"os"
"sort"
"strings"
"testing"
)
func TestV1PublicAPIContract(t *testing.T) {
t.Parallel()
entries, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
fileSet := token.NewFileSet()
var files []*ast.File
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
file, err := parser.ParseFile(fileSet, entry.Name(), nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", entry.Name(), err)
}
files = append(files, file)
}
configuration := types.Config{Importer: importer.Default()}
checked, err := configuration.Check("gamertan.com/sandwich-hime/sando", fileSet, files, nil)
if err != nil {
t.Fatal(err)
}
qualifier := func(pkg *types.Package) string {
if pkg == nil || pkg.Path() == checked.Path() {
return ""
}
return pkg.Name()
}
var actual []string
for _, name := range checked.Scope().Names() {
if !token.IsExported(name) {
continue
}
object := checked.Scope().Lookup(name)
switch object := object.(type) {
case *types.Const:
actual = append(actual, fmt.Sprintf("const %s = %s", object.Name(), object.Val().ExactString()))
case *types.TypeName:
named, ok := object.Type().(*types.Named)
if !ok {
actual = append(actual, types.ObjectString(object, qualifier))
break
}
if structure, ok := named.Underlying().(*types.Struct); ok {
var fields []string
for index := 0; index < structure.NumFields(); index++ {
field := structure.Field(index)
if field.Exported() {
fields = append(fields, field.Name()+" "+types.TypeString(field.Type(), qualifier))
}
}
if len(fields) == 0 {
actual = append(actual, "type "+object.Name()+" struct{ /* opaque */ }")
} else {
actual = append(actual, "type "+object.Name()+" struct{"+strings.Join(fields, "; ")+"}")
}
} else {
actual = append(actual, types.ObjectString(object, qualifier))
}
default:
actual = append(actual, types.ObjectString(object, qualifier))
}
typeName, ok := object.(*types.TypeName)
if !ok {
continue
}
named, ok := typeName.Type().(*types.Named)
if !ok {
continue
}
for index := 0; index < named.NumMethods(); index++ {
method := named.Method(index)
if method.Exported() {
actual = append(actual, types.ObjectString(method, qualifier))
}
}
}
sort.Strings(actual)
got := strings.Join(actual, "\n") + "\n"
want, err := os.ReadFile("testdata/public-api-v1.txt")
if err != nil {
t.Fatal(err)
}
want = []byte(strings.TrimPrefix(string(want), "# SPDX-License-Identifier: Apache-2.0\n\n"))
if string(want) != got {
t.Fatalf("v1 public API drifted\n--- committed contract ---\n%s--- observed API ---\n%s", want, got)
}
}
+119
View File
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: Apache-2.0
package sando
import (
"bytes"
"context"
"html/template"
"io"
"testing"
)
type benchmarkView struct {
Title string
URL string
Admin bool
Items []string
}
var (
benchmarkContext = context.Background()
benchmarkData = benchmarkView{
Title: `A typed <view> & its "output"`,
URL: "/projects/sandwich-hime/?from=benchmark&mode=equivalent",
Admin: true,
Items: []string{"compiler", "runtime", "language server", "editor tooling"},
}
benchmarkHTMLTemplate = template.Must(template.New("v1-corpus").Parse(`<article data-title="{{.Title}}"><h1>{{.Title}}</h1>{{if .Admin}}<strong>Admin</strong>{{end}}<ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul><a href="{{.URL}}">Open</a></article>`))
)
func benchmarkSandoComponent(view benchmarkView) Component {
return ComponentFunc(func(_ context.Context, writer io.Writer) error {
if err := WriteString(writer, `<article data-title="`); err != nil {
return err
}
if err := WriteAttr(writer, view.Title); err != nil {
return err
}
if err := WriteString(writer, `"><h1>`); err != nil {
return err
}
if err := WriteText(writer, view.Title); err != nil {
return err
}
if err := WriteString(writer, `</h1>`); err != nil {
return err
}
if view.Admin {
if err := WriteString(writer, `<strong>Admin</strong>`); err != nil {
return err
}
}
if err := WriteString(writer, `<ul>`); err != nil {
return err
}
for _, item := range view.Items {
if err := WriteString(writer, `<li>`); err != nil {
return err
}
if err := WriteText(writer, item); err != nil {
return err
}
if err := WriteString(writer, `</li>`); err != nil {
return err
}
}
if err := WriteString(writer, `</ul><a href="`); err != nil {
return err
}
if err := WriteURL(writer, view.URL); err != nil {
return err
}
return WriteString(writer, `">Open</a></article>`)
})
}
func TestBenchmarkCorpusEquivalent(t *testing.T) {
t.Parallel()
var himeOutput bytes.Buffer
if err := Render(benchmarkContext, &himeOutput, benchmarkSandoComponent(benchmarkData)); err != nil {
t.Fatal(err)
}
var standardOutput bytes.Buffer
if err := benchmarkHTMLTemplate.Execute(&standardOutput, benchmarkData); err != nil {
t.Fatal(err)
}
if himeOutput.String() != standardOutput.String() {
t.Fatalf("benchmark corpus is not output-equivalent\nhtml/template: %q\nSandwich Hime: %q", standardOutput.String(), himeOutput.String())
}
}
func BenchmarkV1CorpusSandwichHime(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(renderedBenchmarkSize(b)))
for b.Loop() {
if err := Render(benchmarkContext, io.Discard, benchmarkSandoComponent(benchmarkData)); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkV1CorpusHTMLTemplate(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(renderedBenchmarkSize(b)))
for b.Loop() {
if err := benchmarkHTMLTemplate.Execute(io.Discard, benchmarkData); err != nil {
b.Fatal(err)
}
}
}
func renderedBenchmarkSize(tb testing.TB) int {
tb.Helper()
var output bytes.Buffer
if err := Render(benchmarkContext, &output, benchmarkSandoComponent(benchmarkData)); err != nil {
tb.Fatal(err)
}
return output.Len()
}
+225
View File
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: Apache-2.0
package sando
import (
"bytes"
"errors"
"fmt"
"html/template"
"strings"
"testing"
"unicode/utf8"
)
func TestHTMLTemplateDifferentialCorpus(t *testing.T) {
t.Parallel()
tests := []struct {
name string
source string
prefix string
suffix string
write func(*bytes.Buffer, any) error
values []string
}{
{
name: "HTML text", source: `<p>{{.}}</p>`, prefix: `<p>`, suffix: `</p>`,
write: func(output *bytes.Buffer, value any) error { return WriteText(output, value) },
values: differentialTextValues(),
},
{
name: "quoted attribute", source: `<p title="{{.}}">x</p>`, prefix: `<p title="`, suffix: `">x</p>`,
write: func(output *bytes.Buffer, value any) error { return WriteAttr(output, value) },
values: differentialTextValues(),
},
{
name: "RCDATA", source: `<textarea>{{.}}</textarea>`, prefix: `<textarea>`, suffix: `</textarea>`,
write: func(output *bytes.Buffer, value any) error { return WriteRCDATA(output, value) },
values: differentialTextValues(),
},
{
name: "safe URL", source: `<a href="{{.}}">x</a>`, prefix: `<a href="`, suffix: `">x</a>`,
write: func(output *bytes.Buffer, value any) error { return WriteURL(output, value) },
values: []string{"", "/", "./relative", "?q=a&next=b", "#section", "https://example.test/a?x=1&y=2", "HTTP://example.test/", "mailto:reader@example.test"},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
parsed := template.Must(template.New(test.name).Parse(test.source))
for _, value := range test.values {
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, value); err != nil {
t.Fatalf("html/template value %q: %v", value, err)
}
var output bytes.Buffer
output.WriteString(test.prefix)
if err := test.write(&output, value); err != nil {
t.Fatalf("Sandwich Hime value %q: %v", value, err)
}
output.WriteString(test.suffix)
if output.String() != baseline.String() {
t.Fatalf("differential mismatch for %q\nhtml/template: %q\nSandwich Hime: %q", value, baseline.String(), output.String())
}
}
})
}
}
func TestHTMLTemplateDifferentialUnsafeURLPolicy(t *testing.T) {
t.Parallel()
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
values := []string{
"javascript:alert(1)",
" JAVASCRIPT:alert(1) ",
"data:text/html,<script>alert(1)</script>",
"vbscript:msgbox(1)",
"unknown:opaque",
"java%73cript:alert(1)",
}
for _, value := range values {
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, value); err != nil {
t.Fatalf("html/template value %q: %v", value, err)
}
if !strings.Contains(baseline.String(), "#ZgotmplZ") {
t.Fatalf("html/template did not block corpus URL %q: %q", value, baseline.String())
}
var output bytes.Buffer
err := WriteURL(&output, value)
if !errors.Is(err, ErrUnsafeURL) {
t.Fatalf("Sandwich Hime accepted corpus URL %q: output=%q err=%v", value, output.String(), err)
}
if output.Len() != 0 {
t.Fatalf("Sandwich Hime wrote bytes before rejecting %q: %q", value, output.String())
}
}
}
func TestHTMLTemplateDifferentialDocumentedStrictness(t *testing.T) {
t.Parallel()
t.Run("invalid UTF-8", func(t *testing.T) {
value := "invalid UTF-8: \xff:end"
parsed := template.Must(template.New("text").Parse(`<p>{{.}}</p>`))
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, value); err != nil {
t.Fatal(err)
}
if utf8.Valid(baseline.Bytes()) {
t.Fatalf("baseline unexpectedly normalized invalid UTF-8: %q", baseline.Bytes())
}
var output bytes.Buffer
if err := WriteText(&output, value); err != nil {
t.Fatal(err)
}
if !utf8.Valid(output.Bytes()) || !strings.Contains(output.String(), "\uFFFD") {
t.Fatalf("Sandwich Hime did not normalize invalid UTF-8: %q", output.Bytes())
}
})
t.Run("control in otherwise allowed URL", func(t *testing.T) {
value := "https:\n//example.test/"
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, value); err != nil {
t.Fatal(err)
}
if !strings.Contains(strings.ToLower(baseline.String()), "%0a") {
t.Fatalf("baseline did not visibly encode the control: %q", baseline.String())
}
var output bytes.Buffer
if err := WriteURL(&output, value); !errors.Is(err, ErrUnsafeURL) {
t.Fatalf("Sandwich Hime did not fail closed: output=%q err=%v", output.String(), err)
}
if output.Len() != 0 {
t.Fatalf("Sandwich Hime wrote before rejecting the control: %q", output.String())
}
})
t.Run("explicit tel allowlist", func(t *testing.T) {
value := "tel:+15555550100"
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, value); err != nil {
t.Fatal(err)
}
if !strings.Contains(baseline.String(), "#ZgotmplZ") {
t.Fatalf("baseline URL policy changed: %q", baseline.String())
}
var output bytes.Buffer
if err := WriteURL(&output, value); err != nil {
t.Fatalf("Sandwich Hime rejected its documented tel scheme: %v", err)
}
if output.String() != value {
t.Fatalf("Sandwich Hime tel output = %q", output.String())
}
})
}
func TestHTMLTemplateDifferentialExplicitTrustedHTML(t *testing.T) {
t.Parallel()
value := `<strong data-note="reviewed & trusted">ok</strong>`
parsed := template.Must(template.New("trusted HTML").Parse(`<div>{{.}}</div>`))
var baseline bytes.Buffer
if err := parsed.Execute(&baseline, template.HTML(value)); err != nil { // #nosec G203 -- the test is the explicit trust-boundary comparison.
t.Fatal(err)
}
var output bytes.Buffer
output.WriteString("<div>")
if err := WriteText(&output, TrustHTML(value)); err != nil {
t.Fatal(err)
}
output.WriteString("</div>")
if output.String() != baseline.String() {
t.Fatalf("trusted HTML mismatch\nhtml/template: %q\nSandwich Hime: %q", baseline.String(), output.String())
}
}
func differentialTextValues() []string {
return []string{
"",
"ordinary text",
`<script>alert("x")</script>`,
`quotes: "double" and 'single' & ampersand`,
"Unicode: 雪 🥪 e\u0301",
"NUL:\x00:end",
"line separators: \u2028\u2029",
}
}
func FuzzWriteURLPolicy(f *testing.F) {
for _, seed := range []string{
"",
"/relative?one=1&two=2",
"https://example.test/path",
" JAVASCRIPT:alert(1) ",
"https:\n//example.test/",
"tel:+15555550100",
"invalid:\xff",
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, value string) {
if len(value) > 64<<10 {
t.Skip()
}
var first, second bytes.Buffer
firstErr := WriteURL(&first, value)
secondErr := WriteURL(&second, value)
if first.String() != second.String() || fmt.Sprint(firstErr) != fmt.Sprint(secondErr) {
t.Fatal("URL policy was not deterministic")
}
if firstErr != nil {
if !errors.Is(firstErr, ErrUnsafeURL) || first.Len() != 0 {
t.Fatalf("URL rejection was not fail-closed: output=%q err=%v", first.String(), firstErr)
}
return
}
if !utf8.ValidString(first.String()) || strings.ContainsAny(first.String(), "\x00\r\n") {
t.Fatalf("accepted URL output is not valid single-line UTF-8: %q", first.String())
}
})
}
+32
View File
@@ -0,0 +1,32 @@
# SPDX-License-Identifier: Apache-2.0
const ABI = "sando.v1"
const ABISandoV1 = "sando.v1"
const RuntimeABI = "sando.v1"
func (*URLSafetyError).Error() string
func (*URLSafetyError).Unwrap() error
func (ComponentFunc).Render(ctx context.Context, w io.Writer) error
func Render(ctx context.Context, w io.Writer, component Component) error
func TrustCSS(value string) TrustedCSS
func TrustHTML(value string) TrustedHTML
func TrustJS(value string) TrustedJS
func TrustURL(value string) TrustedURL
func WriteAttr(w io.Writer, value any) error
func WriteCSS(w io.Writer, value TrustedCSS) error
func WriteHTML(w io.Writer, value TrustedHTML) error
func WriteJS(w io.Writer, value TrustedJS) error
func WriteRCDATA(w io.Writer, value any) error
func WriteString(w io.Writer, value string) error
func WriteText(w io.Writer, value any) error
func WriteURL(w io.Writer, value any) error
type Component interface{Render(context.Context, io.Writer) error}
type ComponentFunc func(context.Context, io.Writer) error
type TrustedCSS struct{ /* opaque */ }
type TrustedHTML struct{ /* opaque */ }
type TrustedJS struct{ /* opaque */ }
type TrustedURL struct{ /* opaque */ }
type URLSafetyError struct{Scheme string; Reason string}
var ErrNilComponent error
var ErrNilContext error
var ErrNilWriter error
var ErrUnsafeURL error
+69
View File
@@ -0,0 +1,69 @@
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Sanitized public source snapshots
`export-public-snapshot.sh` creates a host-neutral filesystem snapshot from a
committed Git tree. It does not initialize a repository, copy `.git`, configure
a remote, commit, tag, push, or publish anything.
Release use requires a clean worktree, a ref resolving exactly to `HEAD`, and
the exact-file policy committed at `scripts/public-snapshot.allow` in that
ref:
```sh
scripts/export-public-snapshot.sh \
--mode release \
--ref HEAD \
--destination ../sandwich-hime-public-review
```
The destination must not exist and its canonical parent must be outside the
source worktree, its worktree-specific Git directory, and its shared Git common
directory. This includes ordinary `.git` directories and linked-worktree
metadata stored elsewhere. The exporter creates a private sibling staging
directory and renames it into place only after all checks pass. It never clears
or replaces an existing destination; failure cleanup is limited to a staging
directory carrying the exporter's ownership marker.
Review mode may use an externally reviewed exact-file policy while changes to
the exporter itself await a commit. Its provenance is conspicuously marked
`review` and is not a release artifact:
```sh
scripts/export-public-snapshot.sh \
--mode review \
--allowlist /path/to/reviewed-exact-files.allow \
--destination ../snapshot-for-review
```
The policy accepts individual files only—never directories or globs. The
export fails for missing/duplicate/forbidden entries, non-regular Git objects,
symlinks, binary or oversized content, aggregate size limits, private developer
filesystem indicators, common private-key/token indicators, database or build
artifacts, and explicitly private integration material. Host workflow folders,
private trees, prototype/history trees, and application-specific integrations
are not in the reviewed policy.
`PUBLIC-SNAPSHOT.sha256` records every exported source file. The deterministic
`PUBLIC-SNAPSHOT.json` records only the project identifier, export policy and
mode, file count, and policy/manifest digests. Private commit and tree IDs,
commit timestamps, author or committer identity, email, hostname, branch name,
remote URL, and checkout path stay outside the exported tree. Filesystem
timestamps are normalized to the Unix epoch. A separate private release ledger
may map the private source commit to the resulting public commit and signed
tags.
When an exported tree is reviewed into an existing public checkout, compare and
copy files by content (for example, checksum-aware synchronization or a fresh
tree replacement). Size-and-modification-time shortcuts are unsafe here because
the exporter deliberately gives every snapshot the same normalized timestamp;
the manifest and provenance records must be verified again before publication.
Run the focused checks with:
```sh
bash scripts/test-public-snapshot.sh
```
The implementation expects Bash, Git, tar, GNU-compatible core utilities, and
a filesystem supporting an atomic rename within the destination parent.
+25 -7
View File
@@ -3,23 +3,41 @@
# Repository verification tools # Repository verification tools
These scripts are intentionally understandable shell rather than a release These scripts are intentionally understandable shell rather than a release
framework with hidden defaults. The maintained verification and release path framework with hidden defaults. The maintained native verification paths are
is Linux. Linux/amd64 and macOS/arm64.
- `verify.sh` runs root and nested-module tests and vet, builds `himesan`, checks the compiler-owned golden output, and proves two generation passes leave the same bytes and unchanged modification times. Set `HIMESAN_RACE=1` for race tests. - `verify.sh` runs root and nested-module tests and vet, builds `himesan`, checks the compiler-owned golden output, and proves two generation passes leave the same bytes and unchanged modification times. Set `HIMESAN_RACE=1` for race tests.
- `check-licenses.sh` enforces the AGPL compiler / Apache runtime boundary and prevents generated application Go from inheriting an AGPL identifier. - `check-licenses.sh` enforces the AGPL compiler / Apache runtime boundary and prevents generated application Go from inheriting an AGPL identifier.
- `release-check.sh --version vX.Y.Z` is a clean-checkout technical preflight, including exact candidate-version and generated-provenance checks. Beta publication follows the narrower prerelease gates in `RELEASE.md`; release candidates and final v1 additionally use `--public` with a human-reviewed `HIMESAN_RELEASE_EVIDENCE_DIR`. The script never tags, pushes, publishes, or deploys. - `test-public-snapshot.sh` proves the exact allowlist, secret/path scanner, legal boundary, deterministic manifest, and no-overwrite export behavior on both maintained native hosts.
- Contract tests bind the exported runtime API, CLI help, JSON/configuration schemas, diagnostic-code inventory, generic component signature, and generated provenance to the reviewed files under `contracts/` and `sando/testdata/`.
- `release-check.sh --version vX.Y.Z` is a clean-checkout technical preflight, including exact candidate-version and generated-provenance checks. Beta publication follows the narrower prerelease gates in `RELEASE.md`; release candidates and final v1 additionally use `--public` with a human-reviewed `HIMESAN_RELEASE_EVIDENCE_DIR` and the four-lane `HIMESAN_NATIVE_EVIDENCE_DIR`. Seal the review directory with `go run ./cmd/himesan-release evidence-manifest`; `verify-native` independently checks every native receipt sidecar, source identity, gate, freshness bound, and generated-output digest. The script never tags, pushes, publishes, or deploys.
- `verify-public-install.sh --version vX.Y.Z` is a post-tag/publication check. It verifies exact `go-get=1` package routes, adds the nested runtime before installing the parent compiler, and exercises fresh direct-fetch and public-proxy caches without interactive Git credentials. - `verify-public-install.sh --version vX.Y.Z` is a post-tag/publication check. It verifies exact `go-get=1` package routes, adds the nested runtime before installing the parent compiler, and exercises fresh direct-fetch and public-proxy caches without interactive Git credentials.
- `package-native.sh` performs two native builds and uses the repository-owned Go packager for a deterministic archive, manifest, SBOM, and checksums. `package-macos.sh` is the explicit Apple Silicon entrypoint used by the release operator.
- `sign-notarize-macos.sh` is a deliberately manual boundary. It requires the explicitly approved unsigned archive digest, uses Cole's Developer ID and Keychain-held notary profile, regenerates provenance and checksums for the changed signed Mach-O bytes, and produces a signed, notarized, and stapled DMG. The native runner receives neither credential.
- `verify-real-browser.sh` is opt-in release evidence. It runs the development client in an actual reviewed Chrome/Chromium binary, exercising CSP-restricted execution, SSE diagnostics, reload, and fragment/API exclusions, then reruns the process cleanup integration cases. Chrome is not a normal build or consumer dependency.
The canonical Linux CI and release preflight also run bounded fuzz sessions for the parser/context compiler and Go-aware delimiter scanner. Seed-corpus execution remains part of ordinary `go test`; the bounded sessions are extra evidence, not a substitute for longer scheduled fuzzing before v1. The canonical Linux and macOS CI gates run the contract and public-snapshot
checks plus bounded fuzz sessions for the parser/context compiler, Go-aware
delimiter scanner, URL policy, and LSP boundaries. The compiler target also
asserts deterministic diagnostics and generated Go, source-digest binding, and
safe source-map directives. Seed-corpus execution remains part of ordinary
`go test`; the bounded sessions are extra evidence, not a substitute for the
long exact-candidate campaign before v1.
The release preflight invokes `govulncheck` from the official Go vulnerability project at the exact module version `golang.org/x/vuln@v1.6.0`. Updating that pin requires reviewing the upstream tag and rerunning the supported Go lines. The release preflight invokes `govulncheck` from the official Go vulnerability project at the exact module version `golang.org/x/vuln@v1.6.0`. Updating that pin requires reviewing the upstream tag and rerunning the supported Go lines.
## Preview automation status ## Preview automation status
Forge workflows are intentionally excluded from the sanitized pre-1.0 public Forge workflows are intentionally excluded from the sanitized pre-1.0 public
snapshot. The private development repository uses pinned Linux runners; the snapshot. The private development repository uses pinned Linux/amd64 and
public source remains independently verifiable with `verify.sh`, the license repository-scoped native macOS/arm64 runners; the public source remains
check, and the Linux release preflight. independently verifiable with the repository scripts and both native release
preflights.
The private `public-candidate-verification` workflow is a release controller,
not public-source evidence by association. It accepts only the exact lowercase
commit currently at canonical public `main`, clones only that fixed Gitea origin without credentials,
and produces receipts naming `gamertan/sandwich-hime`. Development-repository
receipts cannot satisfy the public release preflight.
If Gitea automation is later added to the public repository, pin every external action to a reviewed immutable commit, document its provenance, grant minimum permissions, and keep a local verification path. A secondary forge may host a sanitized, read-only discovery snapshot, but hosted workflows stay disabled there and it does not become a release or contribution authority. If Gitea automation is later added to the public repository, pin every external action to a reviewed immutable commit, document its provenance, grant minimum permissions, and keep a local verification path. A secondary forge may host a sanitized, read-only discovery snapshot, but hosted workflows stay disabled there and it does not become a release or contribution authority.
+7 -4
View File
@@ -54,7 +54,7 @@ check_sha256() {
is_comment_capable_project_file() { is_comment_capable_project_file() {
case "$1" in case "$1" in
COPYRIGHT | */COPYRIGHT | .editorconfig | .gitattributes | .gitignore | *.go | *.mod | *.md | *.sh | *.ps1 | *.yml | *.yaml | *.html | *.css | *.js | *.toml | *.allow) COPYRIGHT | */COPYRIGHT | .editorconfig | .gitattributes | .gitignore | *.go | *.mod | *.md | *.txt | *.sh | *.ps1 | *.yml | *.yaml | *.html | *.css | *.js | *.toml | *.plist | *.allow)
return 0 return 0
;; ;;
*) *)
@@ -67,8 +67,10 @@ list_project_files() {
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git ls-files --cached --others --exclude-standard -z git ls-files --cached --others --exclude-standard -z
else else
find . -type d \( -name .git -o -name vendor -o -name bin -o -name dist -o -name coverage \) -prune -o \ while IFS= read -r -d '' path; do
-type f -print0 | sed -z 's#^\./##' printf '%s\0' "${path#./}"
done < <(find . -type d \( -name .git -o -name vendor -o -name bin -o -name dist -o -name coverage \) -prune -o \
-type f -print0)
fi fi
} }
@@ -152,7 +154,8 @@ while IFS= read -r -d '' path; do
continue continue
;; ;;
*.json) *.json)
fail "$path cannot carry a comment and needs an explicit license-map entry" grep -Fq "\`$path\`" LICENSES.md || \
fail "$path cannot carry a comment and needs an exact license-map entry"
continue continue
;; ;;
esac esac
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
script_root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
project_root=$(CDPATH= cd -- "$script_root/.." && pwd)
# shellcheck source=public-snapshot-lib.sh
source "$script_root/public-snapshot-lib.sh"
# Release safety limits are policy, not caller-tunable settings.
SNAPSHOT_MAX_FILE_BYTES=1048576
SNAPSHOT_MAX_TOTAL_BYTES=16777216
SNAPSHOT_MAX_FILES=2000
usage() {
cat >&2 <<'USAGE'
Usage: export-public-snapshot.sh --destination PATH [options]
Options:
--source PATH Git worktree root (default: repository containing script)
--ref REF Committed source ref (default: HEAD)
--mode MODE release (default) or review
--allowlist PATH Review mode only: audited external exact-file policy
The destination must not exist and must be outside the source worktree. The
exporter creates it atomically and never initializes Git, configures a remote,
commits, tags, pushes, or copies .git.
USAGE
}
source_path=$project_root
source_ref=HEAD
destination=""
mode=release
allowlist_override=""
while [[ $# -gt 0 ]]; do
case $1 in
--source)
[[ $# -ge 2 ]] || { usage; exit 2; }
source_path=$2
shift 2
;;
--ref)
[[ $# -ge 2 ]] || { usage; exit 2; }
source_ref=$2
shift 2
;;
--destination)
[[ $# -ge 2 ]] || { usage; exit 2; }
destination=$2
shift 2
;;
--mode)
[[ $# -ge 2 ]] || { usage; exit 2; }
mode=$2
shift 2
;;
--allowlist)
[[ $# -ge 2 ]] || { usage; exit 2; }
allowlist_override=$2
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
echo "public snapshot: unknown argument: $1" >&2
usage
exit 2
;;
esac
done
[[ -n $destination ]] || { usage; exit 2; }
[[ $mode == release || $mode == review ]] || { echo "public snapshot: mode must be release or review" >&2; exit 2; }
[[ $source_ref != -* && $source_ref != *$'\n'* && $source_ref != *$'\r'* ]] || {
echo "public snapshot: invalid source ref" >&2
exit 2
}
if [[ $mode == release && -n $allowlist_override ]]; then
echo "public snapshot: release mode requires the allowlist committed in the source ref" >&2
exit 2
fi
source_path=$(snapshot_realpath_existing "$source_path")
git_root=$(git -C "$source_path" rev-parse --show-toplevel 2>/dev/null) || {
echo "public snapshot: source is not a Git worktree" >&2
exit 1
}
git_root=$(snapshot_realpath_existing "$git_root")
[[ $source_path == "$git_root" ]] || { echo "public snapshot: --source must name the worktree root" >&2; exit 1; }
git_dir=$(git -C "$git_root" rev-parse --absolute-git-dir 2>/dev/null) || {
echo "public snapshot: cannot resolve source Git metadata directory" >&2
exit 1
}
git_common_dir=$(git -C "$git_root" rev-parse --git-common-dir 2>/dev/null) || {
echo "public snapshot: cannot resolve source Git common directory" >&2
exit 1
}
git_dir=$(snapshot_realpath_existing "$git_dir")
if [[ $git_common_dir != /* ]]; then
git_common_dir=$git_root/$git_common_dir
fi
git_common_dir=$(snapshot_realpath_existing "$git_common_dir")
commit=$(git -C "$git_root" rev-parse --verify "${source_ref}^{commit}" 2>/dev/null) || {
echo "public snapshot: source ref does not resolve to a commit" >&2
exit 1
}
[[ $commit =~ ^[0-9a-f]{40}$ || $commit =~ ^[0-9a-f]{64}$ ]] || {
echo "public snapshot: source commit is not a full object ID" >&2
exit 1
}
if [[ $mode == release ]]; then
head_commit=$(git -C "$git_root" rev-parse --verify HEAD^{commit})
[[ $commit == "$head_commit" ]] || { echo "public snapshot: release ref must resolve to HEAD" >&2; exit 1; }
status=$(git -C "$git_root" status --porcelain=v1 --untracked-files=all)
snapshot_status_is_clean "$status" || {
echo "public snapshot: release source worktree is dirty" >&2
exit 1
}
fi
destination_parent=$(dirname -- "$destination")
destination_name=$(basename -- "$destination")
[[ $destination_name != . && $destination_name != .. && -n $destination_name ]] || {
echo "public snapshot: invalid destination name" >&2
exit 2
}
destination_parent=$(snapshot_realpath_existing "$destination_parent")
[[ -d $destination_parent && ! -L $destination_parent ]] || {
echo "public snapshot: destination parent must be an existing non-symlink directory" >&2
exit 1
}
for protected_root in "$git_root" "$git_dir" "$git_common_dir"; do
if snapshot_path_is_at_or_below "$destination_parent" "$protected_root"; then
echo "public snapshot: destination must be outside the source worktree and Git metadata" >&2
exit 1
fi
done
destination=$destination_parent/$destination_name
[[ ! -e $destination && ! -L $destination ]] || {
echo "public snapshot: destination already exists; refusing to alter it" >&2
exit 1
}
staging=$(mktemp -d "$destination_parent/.${destination_name}.himesan-public-export.XXXXXX")
marker_name=.himesan-public-export-owned
marker=$staging/$marker_name
printf 'owned temporary public snapshot staging directory\n' >"$marker"
cleanup() {
local status=$?
if [[ -n ${staging:-} && -d $staging && -f $marker ]]; then
case $staging in
"$destination_parent"/."$destination_name".himesan-public-export.*)
rm -rf -- "$staging"
;;
esac
fi
exit "$status"
}
trap cleanup EXIT
policy_file=$staging/.himesan-policy-input
if [[ -n $allowlist_override ]]; then
[[ $mode == review ]] || { echo "public snapshot: external policy is review-only" >&2; exit 2; }
[[ -f $allowlist_override && ! -L $allowlist_override ]] || {
echo "public snapshot: external allowlist must be a regular non-symlink file" >&2
exit 1
}
cp -- "$allowlist_override" "$policy_file"
else
policy_path=scripts/public-snapshot.allow
git -C "$git_root" cat-file -e "$commit:$policy_path" 2>/dev/null || {
echo "public snapshot: committed ref lacks $policy_path" >&2
exit 1
}
git -C "$git_root" show "$commit:$policy_path" >"$policy_file"
fi
policy_sha256=$(sha256sum "$policy_file" | awk '{print $1}')
seen_lines=$'\n'
paths=()
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && ${line:0:1} != '#' ]] || continue
if [[ $line == *[[:space:]]* ]] || snapshot_forbidden_path "$line"; then
echo "public snapshot: invalid or forbidden allowlist entry: $line" >&2
exit 1
fi
case $seen_lines in
*$'\n'"$line"$'\n'*)
echo "public snapshot: duplicate allowlist entry: $line" >&2
exit 1
;;
esac
seen_lines=$seen_lines$line$'\n'
record=$(git -C "$git_root" ls-tree "$commit" -- "$line")
[[ -n $record && ${record#*$'\t'} == "$line" && $record != *$'\n'* ]] || {
echo "public snapshot: allowlisted path is absent or ambiguous in source ref: $line" >&2
exit 1
}
read -r object_mode object_type object_id <<<"${record%%$'\t'*}"
[[ $object_type == blob && ($object_mode == 100644 || $object_mode == 100755) ]] || {
echo "public snapshot: allowlisted path is not a regular file: $line" >&2
exit 1
}
blob_size=$(git -C "$git_root" cat-file -s "$object_id")
((blob_size <= SNAPSHOT_MAX_FILE_BYTES)) || {
echo "public snapshot: allowlisted blob is oversized: $line" >&2
exit 1
}
paths+=("$line")
done <"$policy_file"
[[ ${#paths[@]} -gt 0 ]] || { echo "public snapshot: allowlist selected no files" >&2; exit 1; }
sorted_paths=()
while IFS= read -r -d '' path; do
sorted_paths[${#sorted_paths[@]}]=$path
done < <(printf '%s\0' "${paths[@]}" | LC_ALL=C sort -z)
git -C "$git_root" archive --format=tar "$commit" -- "${sorted_paths[@]}" | tar -xf - -C "$staging"
unlink "$policy_file"
exported_count=$(find "$staging" -type f ! -name "$marker_name" | wc -l)
exported_count=${exported_count//[[:space:]]/}
[[ $exported_count -eq ${#sorted_paths[@]} ]] || {
echo "public snapshot: extracted file count does not match allowlist" >&2
exit 1
}
snapshot_validate_export_tree "$staging" "$marker_name"
manifest=$staging/PUBLIC-SNAPSHOT.sha256
manifest_input=$staging/.himesan-manifest-input
(cd "$staging" && find . -type f ! -name "$marker_name" ! -name .himesan-manifest-input -print0 | LC_ALL=C sort -z | xargs -0 sha256sum) >"$manifest_input"
mv "$manifest_input" "$manifest"
manifest_sha256=$(sha256sum "$manifest" | awk '{print $1}')
provenance=$staging/PUBLIC-SNAPSHOT.json
printf '{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"%s","file_count":%s,"allowlist_sha256":"%s","manifest_sha256":"%s"}\n' \
"$mode" "$exported_count" "$policy_sha256" "$manifest_sha256" >"$provenance"
# Normalize filesystem metadata to a public constant as well as normalizing
# content. Private commit IDs, tree IDs, timestamps, identities, refs, remote
# URLs, and checkout paths do not enter the exported tree.
TZ=UTC find "$staging" -exec touch -t 197001010000 {} +
unlink "$marker"
if mv --help 2>&1 | grep -q -- '-T'; then
mv -nT -- "$staging" "$destination"
else
# BSD mv has no -T. The existing-destination preflight above preserves the
# same no-overwrite policy for the local macOS review lane.
mv -n "$staging" "$destination"
fi
if [[ -e $staging || ! -d $destination ]]; then
echo "public snapshot: destination appeared during activation; staging was not published" >&2
exit 1
fi
staging=""
printf 'public_snapshot=%s\ncommit=%s\nfiles=%s\n' "$destination" "$commit" "$exported_count"
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
usage() {
printf 'Usage: scripts/native-gate.sh --repository OWNER/REPOSITORY --go-version goX.Y.Z --runner-version VERSION --runner-name NAME --output DIR\n' >&2
}
repository=''
expected_go=''
runner_version=''
runner_name=''
output=''
while (( $# > 0 )); do
case "$1" in
--repository) repository=$2; shift 2 ;;
--go-version) expected_go=$2; shift 2 ;;
--runner-version) runner_version=$2; shift 2 ;;
--runner-name) runner_name=$2; shift 2 ;;
--output) output=$2; shift 2 ;;
*) usage; exit 2 ;;
esac
done
if [[ ! "$repository" =~ ^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$ || -z "$expected_go" || -z "$runner_version" || -z "$runner_name" || -z "$output" ]]; then
usage
exit 2
fi
if [[ "$(go env GOVERSION)" != "$expected_go" ]]; then
printf 'error: expected %s, found %s\n' "$expected_go" "$(go env GOVERSION)" >&2
exit 1
fi
mkdir -p -- "$output"
./scripts/check-licenses.sh
./scripts/test-public-snapshot.sh
HIMESAN_RACE=1 ./scripts/verify.sh
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=15s
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=15s
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=15s
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=15s
(
cd sando
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=15s
)
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
(
cd sando
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
)
./scripts/verify-consumer.sh
./scripts/package-native.sh --version v0.0.0-verification.1 --output "$output"
artifact=$(find "$output" -maxdepth 1 -type f -name '*.tar.gz' -print -quit)
artifact_sha=$(awk '{print $1}' "$artifact.sha256")
completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
go run ./cmd/himesan-release receipt \
--output "$output/TEND-CI-VERIFICATION.json" \
--repository "$repository" \
--commit "$(git rev-parse HEAD)" \
--tree "$(git rev-parse 'HEAD^{tree}')" \
--goos "$(go env GOOS)" --goarch "$(go env GOARCH)" \
--go-version "$expected_go" --runner-version "$runner_version" \
--runner-name "$runner_name" --artifact-sha256 "$artifact_sha" \
--completed-at "$completed_at" \
--gates test,vet,build,race,generation,contracts,public-snapshot,fuzz,vulnerability,consumer,package \
--generated-files internal/compiler/testdata/golden/basic.sando.go
printf 'Native verification evidence: %s\n' "$output"
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
if [[ "$(uname -s)/$(uname -m)" != Darwin/arm64 ]]; then
printf 'error: the maintained macOS artifact must be built natively on darwin/arm64\n' >&2
exit 1
fi
exec "$(dirname -- "${BASH_SOURCE[0]}")/package-native.sh" "$@"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
usage() { printf 'Usage: scripts/package-native.sh --version vX.Y.Z --output DIR\n' >&2; }
version=''
output=''
while (( $# > 0 )); do
case "$1" in
--version) [[ $# -ge 2 ]] || { usage; exit 2; }; version=$2; shift 2 ;;
--output) [[ $# -ge 2 ]] || { usage; exit 2; }; output=$2; shift 2 ;;
*) usage; exit 2 ;;
esac
done
if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
printf 'error: a canonical v-prefixed release version is required\n' >&2
exit 2
fi
[[ -n "$output" ]] || { usage; exit 2; }
if ! git diff --quiet -- || ! git diff --cached --quiet --; then
printf 'error: native release packaging requires a clean tracked worktree\n' >&2
exit 1
fi
untracked_sources=$(git ls-files --others --exclude-standard -- \
'*.go' '*.sando' 'go.mod' 'go.sum' 'vendor/**' || true)
if [[ -n "$untracked_sources" ]]; then
printf 'error: untracked build inputs prevent trustworthy release provenance:\n%s\n' "$untracked_sources" >&2
exit 1
fi
target="$(go env GOOS)/$(go env GOARCH)"
case "$target" in
darwin/arm64 | linux/amd64) ;;
*) printf 'error: unsupported maintained native target: %s\n' "$target" >&2; exit 1 ;;
esac
if [[ "$(go env GOVERSION)" != go1.26.7 && "$(go env GOVERSION)" != go1.27.0 ]]; then
printf 'error: unsupported release toolchain: %s\n' "$(go env GOVERSION)" >&2
exit 1
fi
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-native-package.XXXXXXXX")
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
cleanup() { rm -rf -- "$temporary"; }
trap cleanup EXIT HUP INT TERM
commit=$(git rev-parse HEAD)
tree=$(git rev-parse 'HEAD^{tree}')
source_date_epoch=$(git show -s --format=%ct HEAD)
go_version=$(go env GOVERSION)
target_os=${target%/*}
target_arch=${target#*/}
linker_flags="-buildid= -X gamertan.com/sandwich-hime/internal/version.Compiler=$version"
for pass in one two; do
CGO_ENABLED=0 GOTOOLCHAIN=local go build -buildvcs=false -trimpath \
-ldflags "$linker_flags" -o "$temporary/himesan-$pass" ./cmd/himesan
done
if ! cmp -s "$temporary/himesan-one" "$temporary/himesan-two"; then
printf 'error: repeated native builds were not byte-identical\n' >&2
exit 1
fi
case "$target" in
darwin/arm64) expected='Mach-O 64-bit executable arm64' ;;
linux/amd64) expected='ELF 64-bit LSB executable, x86-64' ;;
esac
if ! file "$temporary/himesan-one" | grep -Fq "$expected"; then
printf 'error: candidate has the wrong native executable format\n' >&2
file "$temporary/himesan-one" >&2
exit 1
fi
mkdir -p -- "$output"
go run ./cmd/himesan-release package \
--version "$version" --commit "$commit" --tree "$tree" \
--go-version "$go_version" --goos "$target_os" --goarch "$target_arch" \
--binary "$temporary/himesan-one" --output "$output" \
--source-date-epoch "$source_date_epoch"
printf 'Unsigned native package created. Signing and notarization were not performed.\n'
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
# Sourced validation helpers for export-public-snapshot.sh and its focused
# tests. This file deliberately leaves the caller's shell options unchanged.
snapshot_status_is_clean() {
[[ -z $1 ]]
}
snapshot_path_is_at_or_below() {
local candidate=$1
local boundary=$2
[[ $boundary == / || $candidate == "$boundary" || $candidate == "$boundary/"* ]]
}
snapshot_realpath_existing() {
local path=$1
local directory base
if [[ -d $path ]]; then
(CDPATH= cd -- "$path" && pwd -P)
return
fi
if [[ -f $path ]]; then
directory=$(dirname -- "$path")
base=$(basename -- "$path")
directory=$(CDPATH= cd -- "$directory" && pwd -P) || return 1
printf '%s/%s\n' "$directory" "$base"
return
fi
return 1
}
snapshot_forbidden_path() {
local path=$1
local lower
lower=$(printf '%s' "$path" | LC_ALL=C tr '[:upper:]' '[:lower:]')
[[ $path != /* && $path != *\\* && $path != *//* ]] || return 0
[[ $path != . && $path != .. && $path != ../* && $path != */../* && $path != */.. ]] || return 0
[[ $path != *$'\n'* && $path != *$'\r'* && $path != *$'\t'* ]] || return 0
case "/$lower/" in
*/.git/* | */.gitea/* | */.github/* | */private/* | */prototype/* | */prototypes/* | */history/* | */legacy/* | */vendor/* | */bin/* | */dist/* | */coverage/* | */cmd/himetest/* | */cmd/himework/* | */internal/himesan/* | */internal/integration/* | */templates/*)
return 0
;;
esac
case $lower in
go.work | go.work.sum | .env | .env.* | */.env | */.env.* | *.db | *.db-* | *.sqlite | *.sqlite3 | *.pem | *.key | *.p12 | *.pfx | */id_rsa | */id_ed25519 | *credentials* | *.exe | *.dll | *.dylib | *.so | *.a | *.o | *.test | *.prof | *.cover | *.zip | *.tar | *.tar.gz | *.tgz)
return 0
;;
esac
return 1
}
snapshot_validate_export_tree() {
local root=$1
local marker=${2:-}
local max_file_bytes=${SNAPSHOT_MAX_FILE_BYTES:-1048576}
local max_total_bytes=${SNAPSHOT_MAX_TOTAL_BYTES:-16777216}
local max_files=${SNAPSHOT_MAX_FILES:-2000}
local total=0
local count=0
local file rel size
local users_word=Users
local private_unix="/(home|${users_word})/[^/[:space:]]+"
local private_windows='[A-Za-z]:[\\/]+Users[\\/]'
local private_wsl="/mnt/[a-zA-Z]/${users_word}/"
local pem_begin='-----BEGIN '
local private_key="${pem_begin}([A-Z0-9]+ )?PRIVATE KEY-----|${pem_begin}PGP PRIVATE KEY BLOCK-----"
local provider_token='AKIA[0-9A-Z]{16}|(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}'
local private_commit_id='(private development (source|baseline)|private (development )?(commit|source))[^[:cntrl:]]*[0-9a-f]{12,64}'
local private_repository='sandwich-hime-''dev'
if find "$root" -type l -print -quit | grep -q .; then
echo "public snapshot: symbolic links are forbidden" >&2
return 1
fi
while IFS= read -r -d '' file; do
rel=${file#"$root"/}
[[ -z $marker || $rel != "$marker" ]] || continue
if snapshot_forbidden_path "$rel"; then
echo "public snapshot: forbidden path: $rel" >&2
return 1
fi
size=$(wc -c <"$file")
size=${size//[[:space:]]/}
if ((size > max_file_bytes)); then
echo "public snapshot: oversized file: $rel ($size bytes)" >&2
return 1
fi
total=$((total + size))
count=$((count + 1))
if ((total > max_total_bytes || count > max_files)); then
echo "public snapshot: export exceeds aggregate size/count limits" >&2
return 1
fi
if [[ -s $file ]] && ! LC_ALL=C grep -Iq . "$file"; then
echo "public snapshot: binary file rejected: $rel" >&2
return 1
fi
if LC_ALL=C grep -Eq "$private_unix|$private_windows|$private_wsl" "$file"; then
echo "public snapshot: private filesystem path indicator in $rel" >&2
return 1
fi
if LC_ALL=C grep -Eq -- "$private_key|$provider_token" "$file"; then
echo "public snapshot: key or credential indicator in $rel" >&2
return 1
fi
if LC_ALL=C grep -Eiq -- "$private_commit_id" "$file"; then
echo "public snapshot: private commit identifier in $rel" >&2
return 1
fi
if LC_ALL=C grep -Fq -- "$private_repository" "$file"; then
echo "public snapshot: private repository indicator in $rel" >&2
return 1
fi
done < <(find "$root" -type f -print0 | LC_ALL=C sort -z)
}
+129
View File
@@ -0,0 +1,129 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Exact reviewed file allowlist for a host-neutral public source snapshot.
# Directories, globs, and implicit descendants are intentionally unsupported.
.editorconfig
.gitattributes
.gitignore
AI_CONTRIBUTIONS.md
CHANGELOG.md
CLA.md
CONTRIBUTING.md
COPYRIGHT
DCO.txt
GENERATED_CODE.md
GOVERNANCE.md
LICENSE
LICENSES.md
MAINTAINERS.md
OUTPUT_EXCEPTION.md
README.md
RELEASE.md
ROADMAP.md
SECURITY.md
SPEC.md
TRADEMARKS.md
cmd/himesan/contract_test.go
cmd/himesan/main.go
cmd/himesan/main_test.go
cmd/himesan-release/main.go
contracts/diagnostic-codes-v1.txt
contracts/generated-provenance-v1.txt
contracts/himesan-cli-help-v1.txt
contracts/himesan-config-v1.schema.json
contracts/himesan-operation-output-v1.schema.json
contracts/himesan-version-output-v1.schema.json
docs/ARCHITECTURE.md
docs/BENCHMARKS.md
docs/BRAND.md
docs/COMPATIBILITY.md
docs/DEVELOPMENT_SERVER.md
docs/DIAGNOSTICS.md
docs/LANGUAGE_SERVER.md
docs/SECURITY_EVIDENCE.md
docs/THREAT_MODEL.md
docs/V1_RELEASE_PLAN.md
go.mod
internal/compiler/abi_test.go
internal/compiler/analysis.go
internal/compiler/analysis_test.go
internal/compiler/backend.go
internal/compiler/compiler_test.go
internal/compiler/contract_test.go
internal/compiler/context.go
internal/compiler/diagnostic.go
internal/compiler/discover.go
internal/compiler/e2e_test.go
internal/compiler/fuzz_test.go
internal/compiler/model.go
internal/compiler/operation.go
internal/compiler/parse.go
internal/compiler/replace_unix.go
internal/compiler/replace_windows.go
internal/compiler/testdata/golden/basic.sando
internal/compiler/testdata/golden/basic.sando.go
internal/devserver/browser_evidence_test.go
internal/devserver/config.go
internal/devserver/config_test.go
internal/devserver/contract_test.go
internal/devserver/events.go
internal/devserver/process.go
internal/devserver/process_test.go
internal/devserver/process_unix.go
internal/devserver/process_windows.go
internal/devserver/process_windows_32.go
internal/devserver/process_windows_64.go
internal/devserver/proxy.go
internal/devserver/proxy_test.go
internal/devserver/supervisor.go
internal/devserver/supervisor_test.go
internal/devserver/watch.go
internal/devserver/watch_test.go
internal/lsp/features.go
internal/lsp/protocol.go
internal/lsp/protocol_test.go
internal/lsp/security_test.go
internal/lsp/server.go
internal/lsp/server_test.go
internal/lsp/workspace.go
internal/releaseartifact/artifact.go
internal/releaseartifact/artifact_test.go
internal/releaseartifact/evidence.go
internal/releaseartifact/evidence_test.go
internal/releaseartifact/macos.go
internal/releaseartifact/macos_test.go
internal/releaseartifact/native.go
internal/releaseartifact/native_test.go
internal/testpath/tempdir.go
internal/testpath/tempdir_test.go
internal/version/version.go
internal/version/version_test.go
sando/COPYRIGHT
sando/LICENSE
sando/README.md
sando/api_contract_test.go
sando/benchmark_test.go
sando/component.go
sando/component_test.go
sando/differential_test.go
sando/go.mod
sando/trust.go
sando/write.go
sando/write_test.go
sando/testdata/public-api-v1.txt
scripts/README.md
scripts/PUBLIC_SNAPSHOT.md
scripts/check-licenses.sh
scripts/export-public-snapshot.sh
scripts/native-gate.sh
scripts/package-macos.sh
scripts/package-native.sh
scripts/public-snapshot-lib.sh
scripts/public-snapshot.allow
scripts/release-check.sh
scripts/sign-notarize-macos.sh
scripts/test-public-snapshot.sh
scripts/verify-consumer.sh
scripts/verify-public-install.sh
scripts/verify-real-browser.sh
scripts/verify.sh
+32 -20
View File
@@ -16,9 +16,11 @@ artifacts in the repository, pushes, or deploys.
--version Candidate compiler version. --version Candidate compiler version.
--runtime-tag Existing runtime tag retained by a compiler-only release. --runtime-tag Existing runtime tag retained by a compiler-only release.
Omit only when publishing a matching new runtime tag. Omit only when publishing a matching new runtime tag.
--public Require the human-reviewed RC/final launch evidence bundle named --public Require the human-reviewed RC/final launch evidence bundle
by HIMESAN_RELEASE_EVIDENCE_DIR. Canonical beta prereleases may named by HIMESAN_RELEASE_EVIDENCE_DIR and the four native
run their narrower publication preflight without this flag. runner receipts named by HIMESAN_NATIVE_EVIDENCE_DIR.
Canonical beta prereleases may run their narrower publication
preflight without this flag.
EOF EOF
} }
@@ -171,6 +173,7 @@ if (( compiler_only == 1 )); then
fi fi
artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX") artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX")
artifact_dir=$(CDPATH= cd -- "$artifact_dir" && pwd -P)
cleanup() { cleanup() {
if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then
rm -rf -- "$artifact_dir" rm -rf -- "$artifact_dir"
@@ -238,6 +241,7 @@ for pass in 1 2; do
done done
./scripts/check-licenses.sh ./scripts/check-licenses.sh
./scripts/test-public-snapshot.sh
HIMESAN_RACE=1 ./scripts/verify.sh HIMESAN_RACE=1 ./scripts/verify.sh
printf '\n==> bounded compiler fuzz gates\n' printf '\n==> bounded compiler fuzz gates\n'
@@ -245,6 +249,10 @@ go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s
(
cd sando
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=20s
)
printf '\n==> vulnerability scan (pinned golang.org/x/vuln v1.6.0)\n' printf '\n==> vulnerability scan (pinned golang.org/x/vuln v1.6.0)\n'
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
@@ -253,15 +261,16 @@ go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
) )
printf '\n==> building supported Linux release binary\n' target="$(go env GOOS)/$(go env GOARCH)"
for target in \ case "$target" in
linux/amd64; do linux/amd64 | darwin/arm64) ;;
*) printf 'error: release preflight requires a maintained native target, found %s\n' "$target" >&2; exit 1 ;;
esac
target_os=${target%/*} target_os=${target%/*}
target_arch=${target#*/} target_arch=${target#*/}
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" \ printf '\n==> building supported native %s release binary\n' "$target"
go build -trimpath -ldflags "$compiler_linker_flags" \ CGO_ENABLED=0 go build -trimpath -ldflags "$compiler_linker_flags" \
-o "$artifact_dir/himesan-$target_os-$target_arch" ./cmd/himesan -o "$artifact_dir/himesan-$target_os-$target_arch" ./cmd/himesan
done
for required in \ for required in \
scripts/verify-public-install.sh \ scripts/verify-public-install.sh \
@@ -274,23 +283,26 @@ done
if (( public_release == 1 )); then if (( public_release == 1 )); then
evidence_dir=${HIMESAN_RELEASE_EVIDENCE_DIR:-} evidence_dir=${HIMESAN_RELEASE_EVIDENCE_DIR:-}
native_evidence_dir=${HIMESAN_NATIVE_EVIDENCE_DIR:-}
if [[ -z "$evidence_dir" || ! -d "$evidence_dir" ]]; then if [[ -z "$evidence_dir" || ! -d "$evidence_dir" ]]; then
printf 'error: --public requires HIMESAN_RELEASE_EVIDENCE_DIR\n' >&2 printf 'error: --public requires HIMESAN_RELEASE_EVIDENCE_DIR\n' >&2
exit 1 exit 1
fi fi
for evidence in \ if [[ -z "$native_evidence_dir" || ! -d "$native_evidence_dir" ]]; then
legal-review.md \ printf 'error: --public requires HIMESAN_NATIVE_EVIDENCE_DIR\n' >&2
linux-platform.md \
security.md \
development-supervisor.md \
benchmark-methodology.md \
vanity-imports.md \
signing-and-recovery.md; do
if [[ ! -s "$evidence_dir/$evidence" ]]; then
printf 'error: public release evidence is missing or empty: %s\n' "$evidence_dir/$evidence" >&2
exit 1 exit 1
fi fi
done go run ./cmd/himesan-release verify-native \
--directory "$native_evidence_dir" \
--repository gamertan/sandwich-hime \
--commit "$(git rev-parse HEAD)" \
--tree "$(git rev-parse 'HEAD^{tree}')"
go run ./cmd/himesan-release verify-evidence \
--directory "$evidence_dir" \
--repository gamertan/sandwich-hime \
--version "$version" \
--commit "$(git rev-parse HEAD)" \
--tree "$(git rev-parse 'HEAD^{tree}')"
fi fi
if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: scripts/sign-notarize-macos.sh --archive FILE --sha256 DIGEST --output DIR --keychain-profile NAME
Run this manually from Cole's signed-in macOS account. It never runs in CI.
It signs the native CLI, creates and signs a DMG, submits that DMG to Apple's
notary service, staples its ticket, and validates the distribution.
EOF
}
archive=''
archive_sha256=''
output=''
profile=''
identity='Developer ID Application: Cole Speelman (5BXR9JCUBL)'
identifier='com.gamertan.sandwich-hime.himesan'
while (( $# > 0 )); do
case "$1" in
--archive) archive=$2; shift 2 ;;
--sha256) archive_sha256=$2; shift 2 ;;
--output) output=$2; shift 2 ;;
--keychain-profile) profile=$2; shift 2 ;;
*) usage; exit 2 ;;
esac
done
if [[ -z "$archive" || -z "$archive_sha256" || -z "$output" || -z "$profile" ]]; then usage; exit 2; fi
if [[ ! "$archive_sha256" =~ ^[0-9a-f]{64}$ ]]; then
printf 'error: --sha256 must be the approved lowercase archive digest\n' >&2
exit 2
fi
if [[ "$(uname -s)/$(uname -m)" != Darwin/arm64 ]]; then
printf 'error: signing must run natively on Apple Silicon macOS\n' >&2
exit 1
fi
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-notarize.XXXXXXXX")
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
cleanup() { rm -rf -- "$temporary"; }
trap cleanup EXIT HUP INT TERM
go run ./cmd/himesan-release extract-macos \
--archive "$archive" --sha256 "$archive_sha256" --output "$temporary"
root=$(find "$temporary" -mindepth 1 -maxdepth 1 -type d -print -quit)
binary="$root/himesan"
[[ -x "$binary" ]] || { printf 'error: archive does not contain executable himesan\n' >&2; exit 1; }
codesign --force --options runtime --timestamp \
--identifier "$identifier" --sign "$identity" "$binary"
codesign --verify --strict --verbose=2 "$binary"
go run ./cmd/himesan-release finalize-macos \
--directory "$root" \
--unsigned-archive-sha256 "$archive_sha256" \
--identity "$identity" --identifier "$identifier" \
--finalized-at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
codesign --verify --strict --verbose=2 "$binary"
mkdir -p -- "$output"
version=$(basename "$root")
dmg="$output/$version.dmg"
if [[ -e "$dmg" || -e "$dmg.sha256" ]]; then
printf 'error: signed distribution output already exists: %s\n' "$dmg" >&2
exit 1
fi
hdiutil create -quiet -fs HFS+ -format UDZO -volname "$version" -srcfolder "$root" "$dmg"
codesign --force --timestamp --sign "$identity" "$dmg"
codesign --verify --strict --verbose=2 "$dmg"
xcrun notarytool submit "$dmg" --keychain-profile "$profile" --wait
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
codesign --verify --strict --verbose=2 "$dmg"
spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg"
dmg_sha256=$(shasum -a 256 "$dmg" | awk '{print $1}')
printf '%s %s\n' "$dmg_sha256" "$(basename "$dmg")" >"$dmg.sha256"
chmod 0444 "$dmg" "$dmg.sha256"
printf 'Signed, notarized, and stapled distribution: %s\n' "$dmg"
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
project_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
# shellcheck source=public-snapshot-lib.sh
source "$project_root/scripts/public-snapshot-lib.sh"
exporter=$project_root/scripts/export-public-snapshot.sh
temporary=$(mktemp -d)
cleanup() {
rm -rf -- "$temporary"
}
trap cleanup EXIT
bash -n "$project_root/scripts/public-snapshot-lib.sh" "$exporter"
if grep -Eq 'git[[:space:]]+(init|commit|tag|push|remote)([[:space:]]|$)' "$exporter"; then
echo "exporter contains a forbidden Git mutation command" >&2
exit 1
fi
for required in COPYRIGHT OUTPUT_EXCEPTION.md sando/COPYRIGHT; do
grep -Fxq "$required" "$project_root/scripts/public-snapshot.allow" || {
echo "required legal boundary is absent from public allowlist: $required" >&2
exit 1
}
done
snapshot_status_is_clean ""
if snapshot_status_is_clean " M reviewed.go"; then
echo "dirty status was accepted" >&2
exit 1
fi
snapshot_path_is_at_or_below /var/tmp / || {
echo "filesystem-root boundary did not contain an absolute path" >&2
exit 1
}
if snapshot_path_is_at_or_below /safe-ish /safe; then
echo "path boundary accepted a sibling prefix" >&2
exit 1
fi
for path in .gitea/workflows/verify.yml .github/workflows/verify.yml private/notes.md \
internal/integration/product/test.go go.work build/output.exe data/private.db history/prototype.go; do
if ! snapshot_forbidden_path "$path"; then
echo "private/build path was not rejected: $path" >&2
exit 1
fi
done
snapshot_forbidden_path README.md && { echo "safe path was rejected" >&2; exit 1; }
# Exercise the scanner against the complete proposed policy, including these
# uncommitted exporter files, so the next clean commit cannot reveal a
# self-triggering detector or a missing reviewed path.
proposed_tree=$temporary/proposed
mkdir -p "$proposed_tree"
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && ${line:0:1} != '#' ]] || continue
[[ -f $project_root/$line && ! -L $project_root/$line ]] || {
echo "reviewed allowlist path is missing or not regular: $line" >&2
exit 1
}
mkdir -p "$proposed_tree/$(dirname -- "$line")"
cp -p -- "$project_root/$line" "$proposed_tree/$line"
done <"$project_root/scripts/public-snapshot.allow"
snapshot_validate_export_tree "$proposed_tree"
(cd "$proposed_tree" && bash scripts/check-licenses.sh)
private_home_pattern='/'home'/'cole
private_commit_pattern='80bed136''75e8'
private_tag_pattern='prototype-''2025'
if LC_ALL=C grep -IRq -e "$private_home_pattern" -e "$private_commit_pattern" -e "$private_tag_pattern" "$proposed_tree"; then
echo "public allowlist contains a private path or history identifier" >&2
exit 1
fi
safe_tree=$temporary/safe
mkdir -p "$safe_tree"
printf 'ordinary reviewed source\n' >"$safe_tree/source.go"
snapshot_validate_export_tree "$safe_tree"
empty_tree=$temporary/empty
mkdir -p "$empty_tree"
: >"$empty_tree/empty.txt"
snapshot_validate_export_tree "$empty_tree"
symlink_tree=$temporary/symlink
mkdir -p "$symlink_tree"
printf 'target\n' >"$symlink_tree/target"
ln -s target "$symlink_tree/link"
if snapshot_validate_export_tree "$symlink_tree" >/dev/null 2>&1; then
echo "symlink tree was accepted" >&2
exit 1
fi
oversized_tree=$temporary/oversized
mkdir -p "$oversized_tree"
printf '123456789\n' >"$oversized_tree/large.txt"
if SNAPSHOT_MAX_FILE_BYTES=8 snapshot_validate_export_tree "$oversized_tree" >/dev/null 2>&1; then
echo "oversized file was accepted" >&2
exit 1
fi
binary_tree=$temporary/binary
mkdir -p "$binary_tree"
printf 'text\000binary\n' >"$binary_tree/blob.dat"
if snapshot_validate_export_tree "$binary_tree" >/dev/null 2>&1; then
echo "binary file was accepted" >&2
exit 1
fi
private_tree=$temporary/private
mkdir -p "$private_tree"
printf '/%s/%s/project/private.db\n' home developer >"$private_tree/path.txt"
if snapshot_validate_export_tree "$private_tree" >/dev/null 2>&1; then
echo "private filesystem path was accepted" >&2
exit 1
fi
private_commit_tree=$temporary/private-commit
mkdir -p "$private_commit_tree"
printf 'Private development source: %040d\n' 0 >"$private_commit_tree/ledger.txt"
if snapshot_validate_export_tree "$private_commit_tree" >/dev/null 2>&1; then
echo "private commit identifier was accepted" >&2
exit 1
fi
private_repository_tree=$temporary/private-repository
mkdir -p "$private_repository_tree"
printf 'gamertan/%s%s\n' 'sandwich-hime-' 'dev' >"$private_repository_tree/source.txt"
if snapshot_validate_export_tree "$private_repository_tree" >/dev/null 2>&1; then
echo "private repository identifier was accepted" >&2
exit 1
fi
credential_tree=$temporary/credential
mkdir -p "$credential_tree"
printf '%s%s\n' '-----BEGIN ' 'PRIVATE KEY-----' >"$credential_tree/secret.txt"
if snapshot_validate_export_tree "$credential_tree" >/dev/null 2>&1; then
echo "private key indicator was accepted" >&2
exit 1
fi
# Until these new exporter files themselves are committed, construct a
# review-only policy containing the intersection of the reviewed policy and
# the selected committed source ref. No repository or Git object is mutated.
review_policy=$temporary/review.allow
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && ${line:0:1} != '#' ]] || continue
if git -C "$project_root" cat-file -e "HEAD:$line" 2>/dev/null &&
git -C "$project_root" diff --quiet HEAD -- "$line"; then
printf '%s\n' "$line" >>"$review_policy"
fi
done <"$project_root/scripts/public-snapshot.allow"
# A destination beneath the source worktree (including .git) must fail before
# staging creation. This test never removes anything from the source tree.
inside_name=himesan-export-must-not-exist-$$
inside_destination=$project_root/.git/$inside_name
[[ ! -e $inside_destination && ! -L $inside_destination ]] || {
echo "in-worktree destination unexpectedly exists before test" >&2
exit 1
}
if "$exporter" --source "$project_root" --ref HEAD --mode review \
--allowlist "$review_policy" --destination "$inside_destination" >/dev/null 2>&1; then
echo "exporter accepted a destination inside the source worktree" >&2
exit 1
fi
[[ ! -e $inside_destination && ! -L $inside_destination ]] || {
echo "failed in-worktree export created its destination" >&2
exit 1
}
if find "$project_root/.git" -maxdepth 1 -name ".${inside_name}.himesan-public-export.*" -print -quit | grep -q .; then
echo "failed in-worktree export created a staging directory" >&2
exit 1
fi
# A linked worktree stores its private Git directory and shared common Git
# directory outside that worktree root. Neither metadata location may become
# an export destination. The isolated repositories live entirely in $temporary.
linked_main=$temporary/linked-main
linked_worktree=$temporary/linked-worktree
git init -q "$linked_main"
printf 'reviewed linked-worktree source\n' >"$linked_main/source.go"
git -C "$linked_main" add source.go
git -C "$linked_main" -c user.name='Snapshot Test' -c user.email='snapshot@example.invalid' \
commit -qm 'seed isolated exporter test'
git -C "$linked_main" worktree add -q --detach "$linked_worktree" HEAD
linked_policy=$temporary/linked.allow
printf 'source.go\n' >"$linked_policy"
linked_git_dir=$(git -C "$linked_worktree" rev-parse --absolute-git-dir)
linked_common_dir=$(git -C "$linked_worktree" rev-parse --git-common-dir)
if [[ $linked_common_dir != /* ]]; then
linked_common_dir=$linked_worktree/$linked_common_dir
fi
linked_common_dir=$(snapshot_realpath_existing "$linked_common_dir")
assert_metadata_destination_rejected() {
local label=$1
local parent=$2
local name=$3
local rejected_destination=$parent/$name
[[ ! -e $rejected_destination && ! -L $rejected_destination ]] || {
echo "$label destination unexpectedly exists before test" >&2
exit 1
}
if "$exporter" --source "$linked_worktree" --ref HEAD --mode review \
--allowlist "$linked_policy" --destination "$rejected_destination" >/dev/null 2>&1; then
echo "exporter accepted destination inside $label" >&2
exit 1
fi
[[ ! -e $rejected_destination && ! -L $rejected_destination ]] || {
echo "failed $label export created its destination" >&2
exit 1
}
if find "$parent" -maxdepth 1 -name ".${name}.himesan-public-export.*" -print -quit | grep -q .; then
echo "failed $label export created a staging directory" >&2
exit 1
fi
}
assert_metadata_destination_rejected 'linked-worktree Git directory' "$linked_git_dir" linked-private-destination
assert_metadata_destination_rejected 'shared Git common directory' "$linked_common_dir" linked-common-destination
first=$temporary/public-one
second=$temporary/public-two
"$exporter" --source "$project_root" --ref HEAD --mode review \
--allowlist "$review_policy" --destination "$first" >/dev/null
"$exporter" --source "$project_root" --ref HEAD --mode review \
--allowlist "$review_policy" --destination "$second" >/dev/null
diff -r --no-dereference "$first" "$second" >/dev/null
(cd "$first" && sha256sum -c PUBLIC-SNAPSHOT.sha256 >/dev/null)
for excluded in .git .gitea .github private history internal/integration go.work; do
[[ ! -e $first/$excluded && ! -L $first/$excluded ]] || {
echo "excluded path reached snapshot: $excluded" >&2
exit 1
}
done
[[ -f $first/sando/component.go ]] || { echo "reviewed runtime source was not exported" >&2; exit 1; }
[[ -f $first/PUBLIC-SNAPSHOT.json && -f $first/PUBLIC-SNAPSHOT.sha256 ]] || {
echo "public provenance files are missing" >&2
exit 1
}
grep -q '"schema_version":2' "$first/PUBLIC-SNAPSHOT.json"
grep -q '"export_mode":"review"' "$first/PUBLIC-SNAPSHOT.json"
if grep -Fq "$project_root" "$first/PUBLIC-SNAPSHOT.json" \
|| grep -Fq "$project_root" "$first/PUBLIC-SNAPSHOT.sha256" \
|| grep -q '@' "$first/PUBLIC-SNAPSHOT.json" \
|| grep -Eq '"(commit|tree|source_date_epoch)"' "$first/PUBLIC-SNAPSHOT.json"; then
echo "public provenance exposed a checkout path or email" >&2
exit 1
fi
occupied=$temporary/occupied
mkdir -p "$occupied"
printf 'do not delete\n' >"$occupied/owner-marker"
if "$exporter" --source "$project_root" --ref HEAD --mode review \
--allowlist "$review_policy" --destination "$occupied" >/dev/null 2>&1; then
echo "exporter accepted an existing destination" >&2
exit 1
fi
grep -q 'do not delete' "$occupied/owner-marker"
status=$(git -C "$project_root" status --porcelain=v1 --untracked-files=all)
release_destination=$temporary/release
if snapshot_status_is_clean "$status"; then
"$exporter" --source "$project_root" --ref HEAD --mode release \
--destination "$release_destination" >/dev/null
else
if "$exporter" --source "$project_root" --ref HEAD --mode release \
--destination "$release_destination" >"$temporary/release.log" 2>&1; then
echo "release export accepted a dirty source" >&2
exit 1
fi
grep -q 'release source worktree is dirty' "$temporary/release.log"
[[ ! -e $release_destination ]] || { echo "failed release created a destination" >&2; exit 1; }
fi
echo "public snapshot export checks passed"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-consumer.XXXXXXXX")
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
cleanup() { rm -rf -- "$temporary"; }
trap cleanup EXIT HUP INT TERM
mkdir -p "$temporary/golden"
cp "$repo_root/internal/compiler/testdata/golden/basic.sando" "$temporary/golden/page.sando"
GOTOOLCHAIN=local go build -trimpath -o "$temporary/himesan" "$repo_root/cmd/himesan"
"$temporary/himesan" generate "$temporary/golden/page.sando"
(
cd "$temporary"
GOTOOLCHAIN=local go mod init example.test/himesan-consumer
GOTOOLCHAIN=local go mod edit -go=1.25
GOTOOLCHAIN=local go mod edit -replace=gamertan.com/sandwich-hime/sando="$repo_root/sando"
GOTOOLCHAIN=local go mod tidy
GOTOOLCHAIN=local go test ./...
)
printf 'Temporary Go 1.25 consumer compiled generated output successfully.\n'
+1
View File
@@ -101,6 +101,7 @@ if [[ "$browser_status" != 404 ]]; then
fi fi
scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX") scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX")
scratch_dir=$(CDPATH= cd -- "$scratch_dir" && pwd -P)
cleanup() { cleanup() {
if [[ -n "${scratch_dir:-}" && -d "$scratch_dir" ]]; then if [[ -n "${scratch_dir:-}" && -d "$scratch_dir" ]]; then
chmod -R u+w -- "$scratch_dir" 2>/dev/null || true chmod -R u+w -- "$scratch_dir" 2>/dev/null || true
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo_root"
chrome=${HIMESAN_CHROME:-}
if [[ -z "$chrome" ]]; then
case "$(uname -s)" in
Darwin) chrome='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' ;;
Linux)
for candidate in google-chrome-stable google-chrome chromium chromium-browser; do
if command -v "$candidate" >/dev/null 2>&1; then
chrome=$(command -v "$candidate")
break
fi
done
;;
esac
fi
if [[ -z "$chrome" || ! -f "$chrome" || ! -x "$chrome" ]]; then
printf 'error: set HIMESAN_CHROME to a reviewed Chrome or Chromium executable\n' >&2
exit 1
fi
printf 'Real-browser executable: '
"$chrome" --version
printf 'Go toolchain: '
go version
HIMESAN_CHROME="$chrome" go test -count=1 -tags=himesan_browser_evidence \
./internal/devserver -run '^TestRealBrowserDevelopmentClient$' -v
go test -count=1 ./internal/devserver \
-run '^(TestSupervisorBuildsSwapsAndCleansUp|TestSupervisorClearsTargetWhenCurrentApplicationExits)$' -v
+1
View File
@@ -65,6 +65,7 @@ run_module_checks . "compiler module"
log "compiler module: go build" log "compiler module: go build"
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-verify.XXXXXXXX") build_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-verify.XXXXXXXX")
build_dir=$(CDPATH= cd -- "$build_dir" && pwd -P)
cleanup() { cleanup() {
if [[ -n "${build_dir:-}" && -d "$build_dir" ]]; then if [[ -n "${build_dir:-}" && -d "$build_dir" ]]; then
rm -rf -- "$build_dir" rm -rf -- "$build_dir"