security: publish hardened v1 initiative snapshot
Publish the reviewed security policy and evidence, exact runtime ABI enforcement, orphan-output and permission safeguards, dead-upstream cleanup, and the evidence-gated v1 launch plan. This commit is an exact sanitized export from the private development record. Material implementation and review were assisted by OpenAI Codex; Cole Speelman reviewed the changes and accepts human responsibility. Himesan-Output-Permission: v1.0 Signed-off-by: Cole Speelman <gamertan@noreply.localhost>
This commit is contained in:
@@ -1 +1 @@
|
||||
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":77,"allowlist_sha256":"db978285858ba5a1fefeb732d716338d8c652c5fc583f465d08f80b9ec74f0a9","manifest_sha256":"5abb8eaf376ec390da5b1b5d811ec9aa685bf9e47e54086a51d049193738a49b"}
|
||||
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":80,"allowlist_sha256":"e40a56b3431efdd99b3a78c38c387722de347216640383fc849569a817edf5c6","manifest_sha256":"0b27ea55bc1f2083ac07ae777d20735e651c5026888b973bfe02ee764b9487dc"}
|
||||
|
||||
+20
-17
@@ -15,8 +15,8 @@ b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.
|
||||
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
|
||||
327386b40ee9fb92a8568b6a51722578890393fa23051af632f3180385a4e739 ./README.md
|
||||
ce32945cf5f16ab1a0202615bcf2053f46d421dcdd8ff2f1292f95bb5cf4493d ./RELEASE.md
|
||||
29eebbdfcff05d4ba709bf13d45052c6994767303ddb470031620c9987bca53a ./ROADMAP.md
|
||||
2c86f5b983dfeb97a02d46850fa42e18cab1ed23201822aa3c344b9d2e1b0c3f ./SECURITY.md
|
||||
419c334aeb20dc22ceba8d031aaa95314b77125bf3267fd9fdc003e3865f0327 ./ROADMAP.md
|
||||
d70d89db6bf0142a42a95f45537be5a4562258646d36d56bd9a70096ec78ed91 ./SECURITY.md
|
||||
53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md
|
||||
842beff8afa72d120fcad0ac73afb2049d580ff3000975f3b1786c4ade6a14d4 ./TRADEMARKS.md
|
||||
136a6d82db842547b342f8b0c9ffdc7c04f7c9b473b4ef6dca9dbc940cb24b54 ./cmd/himesan/main.go
|
||||
@@ -25,24 +25,27 @@ ce32945cf5f16ab1a0202615bcf2053f46d421dcdd8ff2f1292f95bb5cf4493d ./RELEASE.md
|
||||
9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md
|
||||
5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md
|
||||
35f5b4b7d195a7b5c071d4665505afef189c7b386d4e3079e9ce8a96ace07f3a ./docs/COMPATIBILITY.md
|
||||
e4021b554ebc479954321586012add57a5fbfb58a1f7fce001d5638880912fc6 ./docs/DEVELOPMENT_SERVER.md
|
||||
5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md
|
||||
51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md
|
||||
f1a8e78c5aa521324ad2fcb386512158d0c0f9956e97f9a1bc8f97aa5d5e9844 ./docs/THREAT_MODEL.md
|
||||
04c6b3f93588177a87edbca8f56af0e0f2a7c5ba31936f3174570c8282a1a7c2 ./docs/SECURITY_EVIDENCE.md
|
||||
d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md
|
||||
76ac444771ac0a4f584ee0cf86ebfd34233412e6e511485f6cc90131a9a50387 ./docs/V1_RELEASE_PLAN.md
|
||||
f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod
|
||||
07d161772e9c6eec0dcc12179286e5c686dcabdc4e56a7cb8d112f640b072563 ./internal/compiler/backend.go
|
||||
28123757d27298dd81cf13ebd9242b24556734a35e36c2ac731f2a8475d70d28 ./internal/compiler/compiler_test.go
|
||||
d99ba263bf501ca81ed38ba88216c063d2fc22f4b45a3f28d5105957f449c4de ./internal/compiler/context.go
|
||||
ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go
|
||||
0a624f76214afbed561a6f6490405c5083e49a53ae301c6ede763b78407ec0c7 ./internal/compiler/backend.go
|
||||
7c96a4b31a34201cb9c48a7f0bac1c201865e4ecc7c080691af5ce68b3d7c207 ./internal/compiler/compiler_test.go
|
||||
dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go
|
||||
b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go
|
||||
42ccf512381e130bf593b065dccd7697560fb00240818efde6321c9095f6b4a6 ./internal/compiler/discover.go
|
||||
45562a41ef9ab1116746e4962ce8f93d4d8651e1e468a38122c626f8a34a2874 ./internal/compiler/discover.go
|
||||
f5a6b31416027cb69a61d1a1421cba779ec3accae59c9ba9dd45d2c31b72149e ./internal/compiler/e2e_test.go
|
||||
eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go
|
||||
4c1625114f92f9cc097c2fb1394fa0e4d43a03156f3be0aa537a485ec8243a57 ./internal/compiler/model.go
|
||||
00180df94e3c73e1614eeae387ef00c3cec90e9f64b45a5a148c79408e2d10c2 ./internal/compiler/operation.go
|
||||
d166096f185d76b2698aa3ab3251f00e58f84cdedc3af667e88ddd528ca0cb76 ./internal/compiler/model.go
|
||||
0c7a7a4d6a51a8b58dfe7c12ecd8c608aff639fd6157a9657a0663ceb58c3c8c ./internal/compiler/operation.go
|
||||
d7d8181455d5f37ef9bcc6bdbf86e0630f20e8a5b3b81688d12742687b434c99 ./internal/compiler/parse.go
|
||||
80cf170514a3b955d24440cb086d34e19f3a305510e3c5db95cb897be91f922a ./internal/compiler/replace_unix.go
|
||||
0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go
|
||||
f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando
|
||||
b190a6a8aed288378ea13d12ec06bac68890c473c03c60016f7fa7534f142008 ./internal/compiler/testdata/golden/basic.sando.go
|
||||
63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go
|
||||
eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go
|
||||
99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go
|
||||
eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go
|
||||
@@ -52,10 +55,10 @@ eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/dev
|
||||
c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go
|
||||
6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go
|
||||
1dae73304532faac4aa8cedda5df65c6d199aaafc001708cc96529c07dee0588 ./internal/devserver/process_windows_64.go
|
||||
a9b7649562f39a707214dc4a67c2353bd29b2df7fce7a05cac9a6451d1a0fcca ./internal/devserver/proxy.go
|
||||
54f0fea40c0a19d268dcc33cab35d2c7d12f50134cd73ce9a609288e356f9fa8 ./internal/devserver/proxy_test.go
|
||||
3d85066927da7e88ccb0a05afa261e06568c007711dbd9fd0219569aff59f568 ./internal/devserver/supervisor.go
|
||||
6c53d6d10eaa36d9286471e21c61a9e80d858fa0ccfd47be0e72d1838ee440ea ./internal/devserver/supervisor_test.go
|
||||
7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go
|
||||
aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go
|
||||
e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go
|
||||
b94103cd4b582968cdb0b61b0164f57ade006fa4e5187fcaa05944274192526a ./internal/devserver/supervisor_test.go
|
||||
e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go
|
||||
b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go
|
||||
d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go
|
||||
@@ -63,8 +66,8 @@ d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/ver
|
||||
e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT
|
||||
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE
|
||||
b4a7bffe678a97209881e07989563a5085aa0ead9e1b67306087dac6b97bad70 ./sando/README.md
|
||||
a8131a53016401fe8cc3f1be660983c79bb8d267fabef262aca8c55667908279 ./sando/component.go
|
||||
ff905eacdef265e8ea04a8e462656f37b5f8ccd579b604b917143b0b1a7e5d6e ./sando/component_test.go
|
||||
7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go
|
||||
a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go
|
||||
ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod
|
||||
80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go
|
||||
85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
Unchecked items are release blockers, not aspirational marketing.
|
||||
|
||||
The ordered initiative, repository topology, release-candidate sequence, and
|
||||
definition of confidence are maintained in
|
||||
[docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md).
|
||||
|
||||
## Compiler and runtime
|
||||
|
||||
- [ ] Compiler-owned deterministic golden output repeated across Linux, macOS, and Windows.
|
||||
|
||||
+117
-5
@@ -2,12 +2,124 @@
|
||||
|
||||
# Security policy
|
||||
|
||||
Sandwich Hime is an unsupported public pre-1.0 source preview. No version is yet supported for production use, and the project makes no vulnerability-response SLA or bug-bounty promise.
|
||||
Sandwich Hime is a public pre-1.0 source preview. Security reports are welcome
|
||||
now, even though no version is currently designated as supported for production
|
||||
use. The project would rather receive a careful early report than project
|
||||
confidence it has not earned.
|
||||
|
||||
Do not put undisclosed vulnerability details, credentials, personal data, or a working exploit in a public issue. Until a dedicated confidential address is published, use the repository owner's published Gitea contact method to ask for a private channel without disclosing the issue. If no private contact method is available, retain the details rather than publishing them. A tested confidential contact and documented response targets remain blockers for a supported release.
|
||||
## Supported versions
|
||||
|
||||
The compiler treats templates and embedded Go as trusted source and rendered values as untrusted data. It does not sandbox template authors. The security boundary and known non-goals are specified in [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md).
|
||||
| Version | Security status |
|
||||
| --- | --- |
|
||||
| Public `main` source preview | Best-effort assessment and fixes; interfaces may change |
|
||||
| Versioned releases | None published yet |
|
||||
|
||||
For a future supported release, the intended process includes a private reproducer, regression test, coordinated disclosure when appropriate, checksums, and an advisory. Release artifacts and tags must be signed. Dependencies are minimized and scanned; generation/checking never fetch dependencies or execute project code.
|
||||
This table will name supported release lines once immutable compiler and runtime
|
||||
versions are published. A pre-1.0 release is not a promise of API stability or
|
||||
fitness for a particular application.
|
||||
|
||||
This policy describes the project's current process and limitations; it is not legal advice and does not promise that every report can be accepted, embargoed, or fixed on a particular schedule.
|
||||
## Report a vulnerability privately
|
||||
|
||||
Email **security@sandwichhime.com**. Please do not put an undisclosed
|
||||
vulnerability, working exploit, credential, secret, or personal data in a
|
||||
public issue.
|
||||
|
||||
Helpful reports include:
|
||||
|
||||
- the affected compiler/runtime version or exact commit;
|
||||
- the relevant `.sando` source, generated Go, or development configuration;
|
||||
- a minimal reproducer and the observed security impact;
|
||||
- operating system, architecture, Go version, and browser when relevant;
|
||||
- whether the issue is already public or has a disclosure deadline; and
|
||||
- a safe way to credit the reporter, or a request to remain anonymous.
|
||||
|
||||
Minimize sensitive data. The mailbox is the private reporting route, but
|
||||
ordinary email is not end-to-end encrypted. Do not send production secrets or
|
||||
unnecessary personal data. An encryption key will be published only after its
|
||||
ownership, backup, and recovery procedure have been tested.
|
||||
|
||||
## What to expect
|
||||
|
||||
These are best-effort targets for a founder-maintained project, not an SLA:
|
||||
|
||||
- acknowledge a report within 7 calendar days;
|
||||
- provide an initial severity/scope assessment within 14 calendar days when a
|
||||
reproducible issue is available; and
|
||||
- provide an update at least every 30 calendar days while an accepted report
|
||||
remains unresolved.
|
||||
|
||||
Health, disability, family responsibility, incomplete evidence, or incident
|
||||
complexity may make those targets impossible. If that happens, the maintainer
|
||||
will communicate the delay when safely able rather than inventing certainty.
|
||||
|
||||
For a reproducible accepted vulnerability, the project aims to retain a private
|
||||
reproducer where safe, add a regression test where practical, document affected
|
||||
versions, and agree on a coordinated disclosure plan when appropriate. A fix
|
||||
may be delivered through a new immutable version, a retraction, an advisory, or
|
||||
documentation that narrows an incorrect guarantee. Published tags will not be
|
||||
moved or silently replaced.
|
||||
|
||||
## Scope and trust boundary
|
||||
|
||||
The most useful reports concern:
|
||||
|
||||
- contextual escaping or browser-parser disagreements;
|
||||
- unsafe URL acceptance or trusted-value boundary confusion;
|
||||
- parser, generator, path, symlink, ownership, or atomic-write failures;
|
||||
- generated-code/runtime ABI mismatches;
|
||||
- deterministic-output or source-provenance failures;
|
||||
- development proxy exposure, request-origin controls, process cleanup, or
|
||||
unintended execution; and
|
||||
- dependency, release, signing, checksum, or artifact-integrity problems.
|
||||
|
||||
Templates and embedded Go are trusted application source. Sandwich Hime is not
|
||||
a sandbox for an untrusted template author. Handwritten Go implementations of
|
||||
`sando.Component` and explicit `sando.Trust*` calls are trusted output
|
||||
capabilities. Application routing, authorization, HTTP headers, database
|
||||
security, deployment, and production process isolation remain application
|
||||
responsibilities unless a defect originates in Sandwich Hime itself.
|
||||
|
||||
The complete boundary and known non-goals are maintained in
|
||||
[the threat model](docs/THREAT_MODEL.md). Reproducible assessment results and
|
||||
open gaps are recorded separately in
|
||||
[the security evidence ledger](docs/SECURITY_EVIDENCE.md).
|
||||
|
||||
## Good-faith research
|
||||
|
||||
Good-faith research means making a reasonable effort to:
|
||||
|
||||
- test only systems, repositories, and data you own or are authorized to test;
|
||||
- prefer local reproductions and the smallest proof necessary;
|
||||
- stop if testing risks availability, privacy, data integrity, or another
|
||||
person's account;
|
||||
- avoid persistence, destructive changes, social engineering, spam, denial of
|
||||
service, credential collection, and unnecessary data access;
|
||||
- retain and transmit the minimum sensitive information required; and
|
||||
- allow reasonable time for investigation before public disclosure.
|
||||
|
||||
This policy permits research on local copies of the source. It does not
|
||||
authorize active testing of project-operated websites, Gitea infrastructure,
|
||||
or third-party deployments without separate written permission. Passively
|
||||
observed issues are welcome. It does not create a bug bounty, safe-harbor
|
||||
contract, embargo obligation, or promise that every report can be accepted.
|
||||
The project will not pursue action against research that the maintainer
|
||||
reasonably believes followed this policy in good faith, but cannot bind third
|
||||
parties or override applicable law. When uncertain, contact the security
|
||||
mailbox before testing.
|
||||
|
||||
## Current assurance level
|
||||
|
||||
The code has maintainer-led threat modeling, adversarial unit and integration
|
||||
tests, race testing, bounded fuzz smoke tests, static analysis, dependency
|
||||
inventory, and known-vulnerability scanning. The evidence ledger records those
|
||||
maintainer-run checks against named commits and dates; its results are
|
||||
point-in-time evidence, not continuous assurance. The project has not received
|
||||
an independent security audit, certification, or formal verification. Coverage
|
||||
percentages, passing scanners, and a clean vulnerability database result are
|
||||
evidence of specific checks—not proof that no vulnerability exists.
|
||||
|
||||
Release artifacts and tags are intended to carry signatures, checksums, an
|
||||
SBOM, and exact source/build provenance. Those controls are publication gates
|
||||
until the first versioned release is actually available.
|
||||
|
||||
This policy is practical project guidance, not legal advice.
|
||||
|
||||
@@ -31,4 +31,8 @@ The stable proxy reserves `/__himesan/events` for SSE. It injects a fixed reload
|
||||
|
||||
When an existing CSP is present, the proxy adds the fixed script's SHA-256 source and same-origin SSE connection permission; it does not add `unsafe-inline` or `unsafe-eval`. The proxy and every candidate upstream are literal loopback addresses. Replaced process groups are terminated and waited for on Unix and Windows.
|
||||
|
||||
If the active application exits, the proxy immediately forgets that exact upstream and closes its idle connections. Requests receive the waiting page until another candidate passes its health check; a different process that later acquires the old loopback port is not selected implicitly.
|
||||
|
||||
Loopback is host-local, not user-local. Host, Origin, and Fetch Metadata checks defend against browser cross-site and DNS-rebinding requests, but they are not authentication against another process or account on the same workstation. Run `himesan dev` only on a trusted, single-user development machine and do not place secrets in its diagnostics. It invokes the configured Go toolchain, may fetch dependencies according to the user's Go environment, executes the project binary with the user's inherited environment, and forwards the application's requests and responses. Eligible HTML responses may be buffered up to 16 MiB for reload injection; application request, response, and SSE concurrency limits remain the application's and operating system's responsibility.
|
||||
|
||||
This is not a production proxy, TLS terminator, public preview server, process orchestrator, or deployment system. V1 refuses non-loopback binding.
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Security evidence ledger
|
||||
|
||||
This ledger records what was actually inspected and executed. It is a
|
||||
maintainer-led self-assessment, not an independent audit, certification, formal
|
||||
verification, or guarantee that no vulnerability exists.
|
||||
|
||||
## Assessment identity
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Assessment date | 2026-08-12 |
|
||||
| Public evidence identity | Exact file checksums in the co-published `PUBLIC-SNAPSHOT.sha256`; private/public commit mapping is retained only in the non-exported operational ledger |
|
||||
| Assessment phases | Clean pre-remediation source followed by clean remediated source |
|
||||
| Primary environment | Linux amd64 under WSL, Go 1.26.5 |
|
||||
| Declared minimum Go | Go 1.25 |
|
||||
| Assessor | Project maintainer with AI-assisted code review; human responsibility retained |
|
||||
|
||||
Security remediation discovered during this assessment was committed and the
|
||||
named checks were rerun from a clean source state. Before this ledger can support
|
||||
a versioned release, the complete campaign must be rerun from the exact
|
||||
sanitized public release commit. Private-to-public commit mappings are retained
|
||||
outside the exported source rather than being disclosed here.
|
||||
|
||||
## Observed evidence
|
||||
|
||||
| Property examined | Enforcement or test surface | Result observed on 2026-08-12 |
|
||||
| --- | --- | --- |
|
||||
| Root correctness | `go test -count=1 ./...` | Pass |
|
||||
| Concurrent access | `go test -race -count=1 ./...` | Pass |
|
||||
| Runtime concurrency | `(cd sando && go test -race -count=1 ./...)` | Pass |
|
||||
| Standard static analysis | `go vet ./...` and runtime equivalent | Pass |
|
||||
| Reachable known vulnerabilities | `govulncheck@v1.6.0` on both modules | No vulnerabilities found on 2026-08-12 |
|
||||
| Dependency surface | `go list -m -json all` in both modules | Zero third-party module requirements |
|
||||
| Statement coverage | Go cover profiles on remediated source | compiler 75.2%; devserver 80.2%; runtime 96.6% |
|
||||
| Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found |
|
||||
| Deterministic generation | repeated generate/check/hash/mtime gates | 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 |
|
||||
| URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases |
|
||||
| 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 |
|
||||
| Native platform behavior | Linux execution; Windows/macOS cross-compilation | Native Windows/macOS execution not yet evidenced |
|
||||
|
||||
Coverage measures statements executed by tests. It is not branch completeness
|
||||
and is not evidence that the executed behavior is secure.
|
||||
|
||||
`govulncheck` reports vulnerabilities known to the Go vulnerability database
|
||||
and reachable through its analysis. A clean result cannot detect unknown flaws,
|
||||
design errors, or vulnerabilities outside its model.
|
||||
|
||||
## Security-relevant design evidence
|
||||
|
||||
### Production boundary
|
||||
|
||||
The production `sando` module contains rendering contracts and contextual write
|
||||
helpers. It contains no HTTP server, router, middleware, template discovery,
|
||||
development proxy, plugin loader, or production process manager. Both compiler
|
||||
and runtime modules currently have no third-party Go module requirements.
|
||||
|
||||
### Compiler behavior
|
||||
|
||||
Compilation builds and formats outputs in memory before generation writes.
|
||||
Recursive discovery rejects or skips observed symlinks, nested modules, VCS
|
||||
trees, vendor trees, and detected filesystem crossings. Existing non-owned,
|
||||
symlink, and non-regular output files are rejected. Each changed file uses an
|
||||
atomic replacement primitive; the whole set is not a filesystem transaction if
|
||||
a later replacement fails.
|
||||
|
||||
`generate` and `check` do not run project code, invoke the Go toolchain, fetch
|
||||
dependencies, or edit module metadata. `himesan dev` is intentionally separate:
|
||||
it builds and executes trusted project code and may fetch modules under the
|
||||
user's normal Go configuration.
|
||||
|
||||
### Output contexts
|
||||
|
||||
The compiler accepts dynamic values only in its enumerated contexts. It rejects
|
||||
dynamic markup construction, unquoted attributes, event-handler values,
|
||||
dynamic style attributes, unsupported URL lists, foreign content, meta refresh,
|
||||
malformed tags, and unbalanced generated components. Runtime helpers escape
|
||||
ordinary text/attributes, validate ordinary whole-URL schemes before writing,
|
||||
and escape every trusted wrapper in RCDATA.
|
||||
|
||||
Script and style output require opaque trusted types. Those types deliberately
|
||||
move responsibility to trusted application code; they are not sanitizers.
|
||||
|
||||
## Reproduction commands
|
||||
|
||||
Run from a clean canonical checkout. Networked scans contact the Go module proxy
|
||||
and vulnerability database.
|
||||
|
||||
```sh
|
||||
go version
|
||||
git status --short
|
||||
git rev-parse HEAD^{commit} HEAD^{tree}
|
||||
|
||||
./scripts/check-licenses.sh
|
||||
HIMESAN_RACE=1 ./scripts/verify.sh
|
||||
|
||||
go test -count=1 -cover ./...
|
||||
(cd sando && go test -count=1 -cover ./...)
|
||||
|
||||
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
||||
(cd sando && go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...)
|
||||
|
||||
go test ./internal/compiler -run '^$' \
|
||||
-fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s
|
||||
go test ./internal/compiler -run '^$' \
|
||||
-fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s
|
||||
```
|
||||
|
||||
The fuzz targets currently assert process robustness and result bounds. They do
|
||||
not yet prove semantic HTML safety.
|
||||
|
||||
## Assessment findings and remediation status
|
||||
|
||||
The 2026-08-12 assessment identified six concrete gaps. Their current
|
||||
working-tree status is recorded here without rewriting the original baseline:
|
||||
|
||||
| Finding | Current remediation | Executable evidence |
|
||||
| --- | --- | --- |
|
||||
| Generated code named an ABI but did not enforce the exact contract | Generated code now requires the version-specific `sando.ABISandoV1` symbol | `TestGeneratedCodeRequiresVersionedRuntimeABIMarker`; `TestRuntimeABIMarker` |
|
||||
| Deleted or renamed sources could leave owned `.sando.go` orphans invisible to directory-level `check` | Directory discovery now reports owned outputs whose adjacent source is absent or non-regular and blocks the operation before writes | `TestDirectoryOperationsRejectOrphanedOwnedOutputBeforeWrites` |
|
||||
| Component-context prose included arbitrary handwritten implementations in the generated-component guarantee | Runtime documentation, policy, and threat model now classify handwritten components as trusted output capabilities | API documentation plus policy review; generated-balance tests retain their narrower scope |
|
||||
| An exited development child left its former upstream selected | Exit notification now clears only the matching active target immediately, independent of the watcher poll interval | `TestClearTargetOnlyClearsSelectedUpstream`; `TestSupervisorClearsTargetWhenCurrentApplicationExits` |
|
||||
| Trusted-value warnings were described more broadly than their analysis supports | Policy and threat-model copy now call them best-effort lexical audit hints rather than type or taint analysis | Documentation assertion and review |
|
||||
| Public copy implied a completed systematic `html/template` differential campaign | Policy and public security copy now describe fixed adversarial cases and list systematic differential work as open | Documentation assertion and review |
|
||||
|
||||
The remediated clean source passed the race-enabled repository verifier,
|
||||
sanitized-snapshot tests, both bounded fuzz-smoke targets, compiler/runtime
|
||||
known-vulnerability scans, and Windows/macOS cross-compilation on 2026-08-12.
|
||||
Those results do not become release evidence until the changes are committed,
|
||||
exported to the sanitized canonical public tree, and re-run from that exact
|
||||
public commit. Native Windows/macOS execution and the other gaps below remain
|
||||
separate release decisions.
|
||||
|
||||
## Open assurance gaps
|
||||
|
||||
- confidential mailbox delivery and response/recovery procedure must be tested;
|
||||
- release signing, checksum, SBOM, and provenance rehearsal is incomplete;
|
||||
- native Windows/macOS execution remains outstanding;
|
||||
- browser-parser differential and semantic property testing need expansion;
|
||||
- compiler input size, CPU, and memory have no built-in hard budget;
|
||||
- filesystem checks do not defend against a hostile local actor racing path
|
||||
components between inspection and use;
|
||||
- the watcher is a convenience mechanism, not a filesystem-integrity monitor;
|
||||
- human-readable diagnostics can include hostile local filenames or child-tool
|
||||
output and should not be treated as a sanitized log protocol;
|
||||
- development CSP rewriting is convenience, not production CSP validation;
|
||||
- deliberately detached child descendants may evade process-tree cleanup;
|
||||
- rendering has no built-in recursion, output-size, allocation, CPU, panic, or
|
||||
deadline enforcement;
|
||||
- static cycle detection and trust-use warnings are best-effort analyses; and
|
||||
- the project has no independent security audit or bug-bounty program.
|
||||
|
||||
## Interpreting this ledger
|
||||
|
||||
“Pass” means the named command or case produced its expected result in the named
|
||||
environment on the assessment date. It does not mean “secure.” Confidence comes
|
||||
from keeping the boundary small, making risky capabilities explicit, preserving
|
||||
ordinary generated Go for review, publishing reproducible tests, recording
|
||||
failures, and correcting claims when evidence is weaker than the prose.
|
||||
+167
-22
@@ -2,38 +2,183 @@
|
||||
|
||||
# Threat model
|
||||
|
||||
## Trusted
|
||||
This document separates demonstrated behavior from intended release work. It
|
||||
defines the boundary Sandwich Hime can reasonably defend; it is not a claim
|
||||
that the project or an application using it is universally secure.
|
||||
|
||||
- `.sando` files and embedded Go statements;
|
||||
- handwritten application Go;
|
||||
- explicit calls to `sando.TrustHTML`, `TrustURL`, `TrustJS`, and `TrustCSS`;
|
||||
- the selected compiler binary and runtime module version.
|
||||
## Security objective
|
||||
|
||||
## Untrusted
|
||||
For supported HTML contexts, data supplied to a compiler-generated component
|
||||
should remain data. It must not change HTML structure, create executable code,
|
||||
escape a quoted attribute, or introduce a disallowed URL scheme unless trusted
|
||||
application source makes an explicit security-sensitive decision.
|
||||
|
||||
- values supplied to components unless deliberately wrapped in a trusted type;
|
||||
That objective follows the same high-level model documented by Go's
|
||||
`html/template`: template authors are trusted while rendered data is not. The
|
||||
implementations and accepted languages differ. A systematic differential test
|
||||
campaign against `html/template` remains open work; current tests cover fixed
|
||||
adversarial cases and do not establish equivalence.
|
||||
|
||||
## Trusted capabilities
|
||||
|
||||
- `.sando` source, including its static markup and embedded Go statements;
|
||||
- handwritten application Go and values whose formatting methods execute Go;
|
||||
- handwritten implementations of `sando.Component`;
|
||||
- explicit `sando.TrustHTML`, `TrustURL`, `TrustJS`, and `TrustCSS` calls;
|
||||
- the selected compiler binary, Go toolchain, runtime module, and generated Go;
|
||||
- local project code built and executed by `himesan dev`; and
|
||||
- the user account, filesystem, environment, and other processes on the
|
||||
development workstation.
|
||||
|
||||
Template semantics become trusted source when built into an application;
|
||||
templates are not an untrusted-content sandbox. Someone allowed to edit one can
|
||||
execute ordinary Go through the application build and must receive the same
|
||||
trust as any other code contributor. Arbitrary or malformed template bytes
|
||||
remain adversarial input to compiler robustness while they are being inspected.
|
||||
|
||||
A handwritten `sando.Component` is a trusted output capability. It may write
|
||||
arbitrary bytes, change HTML parser context, recurse, block, panic, or perform
|
||||
side effects. Hime-generated components are independently checked for balanced
|
||||
HTML and may be inserted with `<?~` only at an HTML content boundary. The open
|
||||
Go interface does not extend that proof to handwritten implementations.
|
||||
|
||||
The `Trust*` constructors do not sanitize. They record that trusted application
|
||||
code accepts responsibility for the supplied bytes. `himesan check` emits
|
||||
best-effort lexical audit hints for direct constructor/type use visible inside
|
||||
`.sando` source; it is not Go type analysis, taint analysis, or a complete
|
||||
inventory of trust created transitively in handwritten Go.
|
||||
|
||||
## Untrusted inputs
|
||||
|
||||
- ordinary values supplied to generated components;
|
||||
- filenames and directory entries encountered during discovery;
|
||||
- stale or manually modified generated output;
|
||||
- missing, stale, or manually modified generated output;
|
||||
- browser requests reaching the development proxy;
|
||||
- child process output and health failures.
|
||||
- child-process output, exit behavior, and health failures; and
|
||||
- malformed template bytes from a repository being inspected, provided the
|
||||
template is not subsequently built and executed as trusted Go.
|
||||
|
||||
## Guarantees sought by v1
|
||||
## Demonstrated controls
|
||||
|
||||
- Context-sensitive escaping for supported HTML text, quoted attributes, URL attributes, and explicitly trusted script/style values.
|
||||
- Compilation failure for unsupported or ambiguous output contexts.
|
||||
- Dangerous normalized URL schemes fail rendering unless explicitly trusted.
|
||||
- Component calls cannot change the surrounding HTML parser context.
|
||||
- Dynamic `title` and `textarea` content uses a distinct RCDATA writer that escapes even `TrustedHTML`; trusted HTML cannot close those elements.
|
||||
- Writer failures propagate and partial output is visible to the caller as an error; applications can buffer when atomic responses matter.
|
||||
- Generation plans all outputs before atomic replacement, targets only owned files, preserves last-good output on failure, and follows neither symlinks nor nested-module traversal.
|
||||
- `generate` and `check` do not execute project code, invoke Go tooling, fetch dependencies, or alter `go.mod`.
|
||||
These are point-in-time implementation and test observations indexed to named
|
||||
commits in the evidence ledger, not continuous assurance.
|
||||
|
||||
- Untrusted dynamic output is accepted only in supported HTML text, quoted
|
||||
attribute, URL, and RCDATA contexts. Script and style interpolation requires
|
||||
an explicit `TrustedJS` or `TrustedCSS` capability.
|
||||
- Dynamic tag names, attribute names, unquoted values, event-handler values,
|
||||
style attributes, foreign SVG/MathML content, URL lists, `srcdoc`, meta
|
||||
refresh, malformed HTML, and ambiguous parser states are rejected.
|
||||
- Ordinary text and quoted attributes are escaped; `title` and `textarea` use
|
||||
an RCDATA writer that escapes every trusted wrapper as ordinary text.
|
||||
- Ordinary whole URL values are normalized and checked before any bytes are
|
||||
written. Schemes outside `http`, `https`, `mailto`, and `tel` fail unless a
|
||||
`TrustedURL` deliberately bypasses that check.
|
||||
- Writer errors and contract-violating short writes propagate to the caller.
|
||||
- All sources in one operation are parsed/context-checked/formatted in memory
|
||||
before the first output change. Each changed output is replaced atomically,
|
||||
and existing destinations lacking the generated-file ownership marker, plus
|
||||
symlink or non-regular destinations, are rejected. The marker prevents
|
||||
accidents; it is not authentication against a hostile local actor.
|
||||
- Recursive discovery skips VCS, vendor, symlink, nested-module, and detected
|
||||
filesystem boundaries. Explicit files are still subject to no-symlink and
|
||||
regular-file checks.
|
||||
- `generate` and `check` do not invoke the Go toolchain, fetch dependencies,
|
||||
execute project code, or edit `go.mod`. `himesan dev` is a separate command
|
||||
that intentionally does all of generate, build, and execute trusted project
|
||||
code.
|
||||
- The development proxy binds to a literal loopback address, validates Host
|
||||
authority, and checks Origin and Sec-Fetch-Site when those headers are
|
||||
present. These checks are hardening, not a user-authentication boundary. Its
|
||||
injected client is fixed and authorized with a hash rather than
|
||||
`unsafe-inline`.
|
||||
|
||||
Executable tests and their latest maintainer-observed results are indexed in
|
||||
[SECURITY_EVIDENCE.md](SECURITY_EVIDENCE.md).
|
||||
|
||||
## Important limits
|
||||
|
||||
### URLs
|
||||
|
||||
URL checking prevents disallowed or ambiguous schemes; it does not decide
|
||||
whether a destination is authorized or trustworthy. `https:` and relative URLs
|
||||
can still leave an origin, submit data, change a document base, or load active
|
||||
content depending on the element and attribute. Applications must validate
|
||||
destinations and apply tighter policy for sensitive sinks.
|
||||
|
||||
### Trusted raw values
|
||||
|
||||
`TrustedHTML`, `TrustedJS`, and `TrustedCSS` are intentional escape hatches.
|
||||
Their authors must preserve the surrounding HTML parser state, including
|
||||
container-closing and legacy parser-transition sequences. Prefer ordinary data,
|
||||
keep trust conversion beside its validator, and review each use manually.
|
||||
|
||||
### Rendering resources and failures
|
||||
|
||||
The runtime does not impose output-size, recursion, CPU, allocation, or time
|
||||
limits; recover panics; or make an arbitrary `io.Writer` transactional.
|
||||
Applications should render into a buffer when an all-or-error HTTP body matters
|
||||
and should apply their own request deadlines, bounded writers, input limits, and
|
||||
panic policy. A context is passed through components, but generated output does
|
||||
not automatically stop between writes when it is canceled.
|
||||
|
||||
The compiler likewise has no hard source-size, memory, or compile-time budget.
|
||||
Run it only against repositories whose resource use the caller is willing to
|
||||
accept.
|
||||
|
||||
### Filesystem concurrency
|
||||
|
||||
Discovery and generation defend against symlinks and non-regular files observed
|
||||
at their checks. They are not currently a security boundary against a hostile
|
||||
local actor racing path components between inspection and use. Run the compiler
|
||||
inside a trusted workspace and user account. Atomic replacement describes each
|
||||
file's visibility; a multi-file generation is not a filesystem transaction if
|
||||
a later replacement fails.
|
||||
|
||||
The watcher is a development convenience, not a filesystem-integrity monitor.
|
||||
It may miss adversarial changes engineered to preserve the metadata it samples.
|
||||
|
||||
### Development supervisor
|
||||
|
||||
Loopback is host-local, not user-local. The development supervisor has no user
|
||||
authentication boundary against another process/account on the workstation. It
|
||||
builds and executes project code with the user's inherited environment and may
|
||||
fetch dependencies according to the user's Go configuration. It is not a
|
||||
production proxy, public preview host, TLS terminator, deployment system, or
|
||||
safe runner for untrusted repositories.
|
||||
|
||||
The watcher scans configured trees and eligible HTML responses may be buffered
|
||||
up to 16 MiB for reload injection. The application and operating system retain
|
||||
responsibility for request, response, SSE, file-count, and process resource
|
||||
limits.
|
||||
|
||||
Development CSP rewriting is reload convenience, not production CSP
|
||||
validation. Process cleanup is best-effort; a deliberately detached descendant
|
||||
may outlive the process tree the supervisor can identify. Human-readable
|
||||
diagnostics may also contain filenames or child-tool output supplied by a local
|
||||
repository, so terminals and log consumers remain part of the trusted
|
||||
development environment.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Templates are not a sandbox. A malicious template author can write malicious Go in a statement tag. Sandwich Hime does not validate business authorization, prevent unsafe application logic, make an arbitrary `io.Writer` transactional, or secure an application router/server. Trusted constructors are intentionally sharp tools and must remain conspicuous in review and `himesan check` reporting. A `TrustedHTML` fragment must be balanced and context-neutral; `TrustedJS` and `TrustedCSS` authors are responsible for excluding container-closing HTML sequences.
|
||||
Sandwich Hime does not:
|
||||
|
||||
The v1 HTML state machine is deliberately smaller than a browser parser. Any construct it cannot prove safe is rejected rather than guessed. Differential testing against Go `html/template` is a baseline, not a claim of byte-identical output or universal parser equivalence.
|
||||
- sandbox template authors or embedded Go;
|
||||
- sanitize arbitrary trusted HTML, JavaScript, CSS, or URLs;
|
||||
- provide application authentication, authorization, CSRF policy, CSP, routing,
|
||||
database security, TLS, caching, or production process isolation;
|
||||
- type-check all embedded Go during `himesan check` (the normal Go build/test
|
||||
remains required);
|
||||
- detect every dynamic or handwritten component cycle;
|
||||
- guarantee safety under hostile concurrent mutation of the workspace; or
|
||||
- replace independent review, browser testing, vulnerability response, or the
|
||||
consuming application's threat model.
|
||||
|
||||
## Principal attack classes
|
||||
## Open release work
|
||||
|
||||
Tests cover delimiter confusion, malformed HTML, quote/entity injection, event attributes, dangerous and obfuscated URLs, script/style termination, Unicode and NUL handling, component context breaks, import/source-map injection, CRLF and path behavior, symlinks, nested modules, stale outputs, interrupted/read-only writes, writer failures, component cycles, development-proxy exposure, CSP weakening, compression/content-length mistakes, and orphaned child processes.
|
||||
- broaden semantic and browser-parser differential testing;
|
||||
- execute the native Windows/macOS security and process-lifecycle matrix;
|
||||
- complete signed release provenance, checksums, and SBOM evidence;
|
||||
- test the confidential reporting and signing-key recovery procedures; and
|
||||
- close or explicitly accept every finding listed in the evidence ledger before
|
||||
assigning a supported release line.
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# v1.0.0 launch initiative
|
||||
|
||||
Sandwich Hime v1 is a compatibility and evidence milestone, not a reason to
|
||||
accumulate features. The intended product is already visible: an HTML-first,
|
||||
ahead-of-time template engine for Go, typed generated components, a small
|
||||
HTTP-independent runtime, and an optional opinionated local development loop.
|
||||
|
||||
The private development initiative lives on `codex/v1-launch` in
|
||||
`sandwich-hime-dev`. Public releases never originate from that private history.
|
||||
|
||||
## Repository and publication topology
|
||||
|
||||
| Surface | Purpose | History and tags |
|
||||
| --- | --- | --- |
|
||||
| Private Gitea `sandwich-hime-dev` | development, working branches, private review records, and historical context | normal private history; no public release tags |
|
||||
| Public Gitea `sandwich-hime` | canonical sanitized source, contribution venue, module origin, releases, and signed tags | fresh reviewed publication history; authoritative `sando/vX.Y.Z` and `vX.Y.Z` tags |
|
||||
| GitHub `gamertan/sandwich-hime` | discoverability and a convenient sanitized source snapshot | no private refs, force-mirrors, workflows, contribution authority, release artifacts, or semver tags |
|
||||
|
||||
Each public update is exported through the exact committed allowlist, inspected,
|
||||
committed as a fresh public snapshot, and compared byte-for-byte with the
|
||||
reviewed export. GitHub receives that public tree only. It never receives the
|
||||
private repository or an indiscriminate Git mirror.
|
||||
|
||||
## Current readiness
|
||||
|
||||
At the 2026-08-12 v1 initiative baseline, the project is a strong engineering
|
||||
preview, but not yet a release candidate. Exact private/public commit mappings
|
||||
remain in the non-exported operational ledger.
|
||||
|
||||
### Demonstrated now
|
||||
|
||||
- race-enabled tests, vet, builds, deterministic generation, license checks,
|
||||
and sanitized-export tests pass on Linux;
|
||||
- the compiler module and nested `sando` runtime declare zero third-party Go
|
||||
module requirements;
|
||||
- generated/runtime compatibility uses a version-specific compile-time ABI
|
||||
marker with an incompatible-runtime regression;
|
||||
- owned-output, orphan, stale, symlink, nested-module, restrictive-permission,
|
||||
last-good, writer-error, and enumerated contextual-output cases are tested;
|
||||
- the development proxy is loopback-only, browser-origin hardened, and clears
|
||||
a dead selected upstream immediately; and
|
||||
- the security policy, threat model, and dated evidence ledger state both the
|
||||
demonstrated controls and the unresolved limits.
|
||||
|
||||
### Not demonstrated yet
|
||||
|
||||
- native Windows and macOS execution of the complete supported matrix;
|
||||
- a stable public API/CLI/schema snapshot and compatibility test;
|
||||
- systematic browser-parser and `html/template` differential testing;
|
||||
- a long semantic fuzz campaign beyond bounded no-panic smoke;
|
||||
- committed, reproducible comparative benchmarks and a predefined regression
|
||||
threshold;
|
||||
- real-browser SSE/reload/overlay evidence for `himesan dev`;
|
||||
- deterministic release artifacts, checksums, SBOMs, signatures, and tested
|
||||
signing/recovery procedures; or
|
||||
- clean direct and public-proxy installation of signed release tags.
|
||||
|
||||
## Milestone 1: contract freeze
|
||||
|
||||
Required before security/platform release-candidate work is declared complete:
|
||||
|
||||
- [ ] Decide and specify whether generic component function signatures are v1.
|
||||
- [ ] Inventory and freeze every exported `sando` symbol, trusted type,
|
||||
sentinel error, concrete error field, helper, and ABI marker.
|
||||
- [ ] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas,
|
||||
`himesan.json` schema, and generated provenance fields.
|
||||
- [ ] Specify nil/stringification behavior, supported HTML-context matrix,
|
||||
component trust boundary, URL semantics, and explicit unsupported cases.
|
||||
- [ ] Add machine-checked public API, CLI, diagnostic, schema, and generated
|
||||
output compatibility snapshots.
|
||||
- [ ] Define the v1 deprecation and security-support policy.
|
||||
|
||||
## Milestone 2: security and native-platform evidence
|
||||
|
||||
- [ ] Run the minimum supported Go line and the latest two stable Go lines on
|
||||
native Linux, macOS, and Windows hosts.
|
||||
- [ ] Prove identical generated bytes across those hosts and exercise native
|
||||
path, replacement, permission, race, process-tree, and watcher behavior.
|
||||
- [ ] Build a systematic differential corpus against Go's documented
|
||||
`html/template` safety baseline for overlapping supported contexts.
|
||||
- [ ] Parse representative outputs in real browsers and test structure/code
|
||||
invariants rather than only byte equality.
|
||||
- [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports,
|
||||
paths, source maps, URL normalization, and filesystem operations.
|
||||
- [ ] Resolve or explicitly accept every open item in
|
||||
`SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee.
|
||||
- [ ] Test delivery and reply through `security@sandwichhime.com`.
|
||||
- [ ] Define severity, advisory, retraction, and CVE-request handling.
|
||||
|
||||
## Milestone 3: measured performance and development UX
|
||||
|
||||
- [ ] Commit a synthetic, repository-owned benchmark corpus comparing
|
||||
equivalent typed views and output with `html/template`.
|
||||
- [ ] Define “no material regression” before measuring the release candidate;
|
||||
publish hardware, OS, Go version, commands, samples, allocations, and output
|
||||
equivalence with every result.
|
||||
- [ ] Test SSE reconnect, reload, diagnostic overlays, CSP changes, fragment/API
|
||||
exclusions, caching, and child cleanup in a real browser on supported hosts.
|
||||
- [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced
|
||||
reliably instead of substituting prose for a test.
|
||||
|
||||
## Milestone 4: release rehearsal
|
||||
|
||||
- [ ] Make version validation identical in the CLI, generated headers, scripts,
|
||||
and release artifacts; reject ambiguous build metadata.
|
||||
- [ ] Build the candidate compiler at its candidate version and prove its
|
||||
committed outputs are current under that exact binary.
|
||||
- [ ] Produce deterministic archives/binaries, checksums, SBOMs, signatures,
|
||||
and source/build provenance from a clean sanitized canonical checkout.
|
||||
- [ ] Test release-key backup and two-person recovery for Gitea, domains,
|
||||
signing material, and publication instructions.
|
||||
- [ ] Make evidence gates validate content and commit identity rather than only
|
||||
the presence of non-empty files.
|
||||
- [ ] Rehearse runtime-first publication and rollback without creating public
|
||||
semver tags.
|
||||
|
||||
## Milestone 5: release candidates and final launch
|
||||
|
||||
1. Export and review the sanitized canonical release tree.
|
||||
2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same
|
||||
reviewed public Gitea commit.
|
||||
3. Verify documented installs through fresh `GOPROXY=direct` and
|
||||
`proxy.golang.org` caches on supported Go versions and native platforms.
|
||||
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
|
||||
observation period with no unresolved Hime render, security, accessibility,
|
||||
or rollback regression. This is product dogfooding, not a dependency on EQL
|
||||
or another application's private repository.
|
||||
6. Fix findings in a new RC; restart the observation period when the affected
|
||||
boundary warrants it.
|
||||
7. Finalize the changelog, supported-version table, migration notes, release
|
||||
notes, legal/trademark review, checksums, SBOMs, and signatures.
|
||||
8. Publish `sando/v1.0.0` first and `v1.0.0` second. Never move a tag.
|
||||
9. Refresh the untagged GitHub discovery snapshot and point it to canonical
|
||||
Gitea releases and contribution channels.
|
||||
|
||||
## Explicitly deferrable after v1
|
||||
|
||||
Unless testing finds a release-blocking consequence, v1 need not include every
|
||||
possible context, hostile-local filesystem hardening, typed trust-flow analysis,
|
||||
complete dynamic cycle detection, an encrypted reporting key, or an external
|
||||
audit. Those limits must remain visible and must not be contradicted by
|
||||
marketing. New features do not outrank a small stable contract.
|
||||
|
||||
## Definition of confidence
|
||||
|
||||
“Ready for v1” means a reviewer can trace each promise to a stable public
|
||||
contract, executable evidence from supported native environments, and a signed
|
||||
artifact built from the exact canonical source. It does not mean perfect,
|
||||
invulnerable, or finished forever.
|
||||
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeneratedCodeRequiresVersionedRuntimeABIMarker(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping temporary-module ABI compilation in short mode")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
directory := resolvedTempDir(t)
|
||||
templatePath := filepath.Join(directory, "page.sando")
|
||||
mustWrite(t, templatePath, "<?sando go\npackage generated\nfunc Page()\n?>")
|
||||
result, err := Generate(context.Background(), []string{templatePath})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate: %v (%v)", err, result.Diagnostics)
|
||||
}
|
||||
generated := string(mustRead(t, templatePath+".go"))
|
||||
if !strings.Contains(generated, ".ABISandoV1") {
|
||||
t.Fatalf("generated code does not require the sando.v1 marker:\n%s", generated)
|
||||
}
|
||||
|
||||
mustWrite(t, filepath.Join(directory, "go.mod"), `module example.test/abi
|
||||
|
||||
go 1.25
|
||||
|
||||
require gamertan.com/sandwich-hime/sando v0.0.0
|
||||
|
||||
replace gamertan.com/sandwich-hime/sando => ./fake-sando
|
||||
`)
|
||||
mustWrite(t, filepath.Join(directory, "fake-sando", "go.mod"), `module gamertan.com/sandwich-hime/sando
|
||||
|
||||
go 1.25
|
||||
`)
|
||||
mustWrite(t, filepath.Join(directory, "fake-sando", "component.go"), `package sando
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
const ABI = "sando.incompatible"
|
||||
|
||||
type Component interface { Render(context.Context, io.Writer) error }
|
||||
type ComponentFunc func(context.Context, io.Writer) error
|
||||
func (f ComponentFunc) Render(ctx context.Context, w io.Writer) error { return f(ctx, w) }
|
||||
`)
|
||||
|
||||
command := exec.Command("go", "test", "./...")
|
||||
command.Dir = directory
|
||||
command.Env = append(os.Environ(), "GOWORK=off")
|
||||
output, buildErr := command.CombinedOutput()
|
||||
if buildErr == nil {
|
||||
t.Fatalf("generated code compiled against an incompatible runtime:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "undefined: __himesan_sando.ABISandoV1") {
|
||||
t.Fatalf("incompatible runtime failed for an unexpected reason: %v\n%s", buildErr, output)
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,10 @@ func generateGo(file *sourceFile) ([]byte, []Diagnostic) {
|
||||
}
|
||||
}
|
||||
output.WriteString(")\n\n")
|
||||
fmt.Fprintf(&output, "var _ = %s.ABI\n\n", imports.Sando)
|
||||
// A version-specific exported marker makes the generated/runtime ABI a Go
|
||||
// build-time contract. The descriptive ABI string alone cannot enforce
|
||||
// compatibility because constant values are not part of symbol resolution.
|
||||
fmt.Fprintf(&output, "var _ = %s.ABISandoV1\n\n", imports.Sando)
|
||||
fmt.Fprintf(&output, "func %s%s%s %s.Component {\n", file.Name, file.TypeParams, file.Params, imports.Sando)
|
||||
fmt.Fprintf(&output, "\treturn %s.ComponentFunc(func(%s %s.Context, %s %s.Writer) error {\n", imports.Sando, contextName, imports.Context, writerName, imports.IO)
|
||||
fmt.Fprintf(&output, "\t\t_ = %s\n", contextName)
|
||||
|
||||
@@ -340,6 +340,68 @@ func TestGenerateRefusesUnownedOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryOperationsRejectOrphanedOwnedOutputBeforeWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
directory := resolvedTempDir(t)
|
||||
orphanSource := filepath.Join(directory, "orphan.sando")
|
||||
mustWrite(t, orphanSource, simpleSource("Orphan", "last good"))
|
||||
if _, err := Generate(context.Background(), []string{directory}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
orphanOutput := orphanSource + ".go"
|
||||
lastGood := mustRead(t, orphanOutput)
|
||||
if err := os.Remove(orphanSource); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
liveSource := filepath.Join(directory, "live.sando")
|
||||
mustWrite(t, liveSource, simpleSource("Live", "must not be written"))
|
||||
// A handwritten file whose name merely resembles an output is not owned by
|
||||
// Hime-san and must not be treated as an orphan.
|
||||
mustWrite(t, filepath.Join(directory, "handwritten.sando.go"), "package demo\n")
|
||||
|
||||
checked, err := Check(context.Background(), []string{directory})
|
||||
if err == nil {
|
||||
t.Fatalf("orphaned owned output unexpectedly passed check: %+v", checked)
|
||||
}
|
||||
assertDiagnosticCode(t, checked.Diagnostics, "HIM2014")
|
||||
|
||||
generated, err := Generate(context.Background(), []string{directory})
|
||||
if err == nil {
|
||||
t.Fatalf("orphaned owned output unexpectedly allowed generation: %+v", generated)
|
||||
}
|
||||
assertDiagnosticCode(t, generated.Diagnostics, "HIM2014")
|
||||
if !bytes.Equal(lastGood, mustRead(t, orphanOutput)) {
|
||||
t.Fatal("orphaned last-good output changed")
|
||||
}
|
||||
if _, statErr := os.Stat(liveSource + ".go"); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("batch wrote a live output despite the orphan diagnostic: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGeneratedOutputInheritsRestrictiveSourceMode(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("POSIX file mode test")
|
||||
}
|
||||
t.Parallel()
|
||||
directory := resolvedTempDir(t)
|
||||
path := filepath.Join(directory, "private.sando")
|
||||
mustWrite(t, path, simpleSource("Private", "private"))
|
||||
if err := os.Chmod(path, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Generate(context.Background(), []string{path}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := os.Stat(path + ".go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != 0o600 {
|
||||
t.Fatalf("generated output mode = %04o, want 0600", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoveryBoundariesAndExplicitNestedFile(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation commonly requires additional Windows privileges")
|
||||
|
||||
@@ -34,7 +34,8 @@ const (
|
||||
)
|
||||
|
||||
type contextAnalyzer struct {
|
||||
file *sourceFile
|
||||
file *sourceFile
|
||||
positions positionTable
|
||||
|
||||
state htmlState
|
||||
currentTag string
|
||||
@@ -79,7 +80,12 @@ var unsupportedDynamicAttributes = map[string]string{
|
||||
}
|
||||
|
||||
func analyzeContexts(file *sourceFile) []Diagnostic {
|
||||
analyzer := &contextAnalyzer{file: file, state: htmlData, attrFirstDynamic: -1}
|
||||
analyzer := &contextAnalyzer{
|
||||
file: file,
|
||||
positions: newPositionTable(file.Source),
|
||||
state: htmlData,
|
||||
attrFirstDynamic: -1,
|
||||
}
|
||||
var diagnostics []Diagnostic
|
||||
for nodeIndex := range file.Nodes {
|
||||
node := &file.Nodes[nodeIndex]
|
||||
@@ -136,20 +142,20 @@ func analyzeContexts(file *sourceFile) []Diagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
end := analyzer.positions.at(len(file.Source))
|
||||
if analyzer.state != htmlData {
|
||||
diagnostics = append(diagnostics, diagnostic(file.Path, endPosition(file.Source), "HIM1310", "template ends in an incomplete or ambiguous HTML parser context"))
|
||||
diagnostics = append(diagnostics, diagnostic(file.Path, end, "HIM1310", "template ends in an incomplete or ambiguous HTML parser context"))
|
||||
}
|
||||
if len(analyzer.stack) != 0 {
|
||||
diagnostics = append(diagnostics, diagnostic(file.Path, endPosition(file.Source), "HIM1311", fmt.Sprintf("component must finish in its starting HTML context; unclosed <%s>", analyzer.stack[len(analyzer.stack)-1])))
|
||||
diagnostics = append(diagnostics, diagnostic(file.Path, end, "HIM1311", fmt.Sprintf("component must finish in its starting HTML context; unclosed <%s>", analyzer.stack[len(analyzer.stack)-1])))
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func (a *contextAnalyzer) consumeText(text string, start sourcePosition) *Diagnostic {
|
||||
positionTable := newPositionTable(a.file.Source)
|
||||
for index := 0; index < len(text); index++ {
|
||||
b := text[index]
|
||||
position := positionTable.at(start.Offset + index)
|
||||
position := a.positions.at(start.Offset + index)
|
||||
if a.rawTag == "script" {
|
||||
a.scriptTail += string(b)
|
||||
if len(a.scriptTail) > len("<!--") {
|
||||
@@ -614,7 +620,3 @@ func isAttributeNameChar(b byte) bool {
|
||||
func isHTMLSpace(b byte) bool {
|
||||
return b == ' ' || b == '\t' || b == '\r' || b == '\n' || b == '\f'
|
||||
}
|
||||
|
||||
func endPosition(source []byte) sourcePosition {
|
||||
return newPositionTable(source).at(len(source))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -115,7 +117,24 @@ func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".sando" {
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(entry.Name(), ".sando.go") {
|
||||
entryInfo, statErr := entry.Info()
|
||||
if statErr != nil {
|
||||
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2013", "cannot inspect possible generated output: "+statErr.Error()))
|
||||
return nil
|
||||
}
|
||||
if !entryInfo.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
if orphanDiagnostic := inspectOwnedGeneratedOutput(path); orphanDiagnostic != nil {
|
||||
diagnostics = append(diagnostics, *orphanDiagnostic)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(entry.Name()) != ".sando" {
|
||||
return nil
|
||||
}
|
||||
entryInfo, statErr := entry.Info()
|
||||
@@ -157,6 +176,50 @@ func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) {
|
||||
return discovered, diagnostics
|
||||
}
|
||||
|
||||
func inspectOwnedGeneratedOutput(path string) *Diagnostic {
|
||||
owned, err := hasGeneratedMarker(path)
|
||||
if err != nil {
|
||||
item := diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2013", "cannot inspect possible generated output: "+err.Error())
|
||||
return &item
|
||||
}
|
||||
if !owned {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourcePath := strings.TrimSuffix(path, ".go")
|
||||
info, err := os.Lstat(sourcePath)
|
||||
if err == nil && info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
message := "owned generated output is orphaned because its adjacent .sando source is missing; review and remove the output explicitly"
|
||||
if err == nil {
|
||||
message = "owned generated output is orphaned because its adjacent .sando source is not a regular file; review and remove the output explicitly"
|
||||
} else if !os.IsNotExist(err) {
|
||||
message = "cannot inspect the adjacent .sando source for an owned generated output: " + err.Error()
|
||||
}
|
||||
item := diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2014", message)
|
||||
return &item
|
||||
}
|
||||
|
||||
func hasGeneratedMarker(path string) (bool, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
marker := []byte(generatedPrefix + "\n")
|
||||
prefix := make([]byte, len(marker))
|
||||
if _, err := io.ReadFull(file, prefix); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return string(prefix) == string(marker), nil
|
||||
}
|
||||
|
||||
func firstSymlinkComponent(path string) (string, error) {
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -86,6 +86,7 @@ type CompiledFile struct {
|
||||
Digest string
|
||||
Code []byte
|
||||
source *sourceFile
|
||||
sourceMode uint32
|
||||
}
|
||||
|
||||
// FileResult describes one source/output pair processed by Generate or Check.
|
||||
|
||||
@@ -61,7 +61,10 @@ func Generate(ctx context.Context, paths []string) (Result, error) {
|
||||
result.Unchanged++
|
||||
continue
|
||||
}
|
||||
mode := os.FileMode(0o644)
|
||||
// Generated Go can contain every literal present in its source. A new
|
||||
// output therefore must not be more permissive than the source file.
|
||||
// Execute bits are never meaningful for Go source and are stripped.
|
||||
mode := os.FileMode(file.sourceMode) & 0o666
|
||||
if info, statErr := os.Stat(file.OutputPath); statErr == nil {
|
||||
mode = info.Mode().Perm()
|
||||
}
|
||||
@@ -151,6 +154,7 @@ func compileOperation(ctx context.Context, paths []string) ([]CompiledFile, Resu
|
||||
output, diagnostics := compileWithMapping(sourcePath, source, moduleRelativeSourcePath(sourcePath))
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostics...)
|
||||
if output.Code != nil {
|
||||
output.sourceMode = uint32(info.Mode().Perm())
|
||||
compiled = append(compiled, output)
|
||||
result.Files = append(result.Files, FileResult{SourcePath: output.SourcePath, OutputPath: output.OutputPath})
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
var _ = __himesan_sando.ABISandoV1
|
||||
|
||||
func Greeting(name string) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
|
||||
@@ -104,6 +104,22 @@ func (d *developmentProxy) setTarget(address string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearTarget forgets address only when it is still the selected upstream.
|
||||
// The compare-and-swap prevents a concurrently replaced target from being
|
||||
// cleared between the load and store. The supervisor serializes activation
|
||||
// and exit handling and calls this only for its current candidate.
|
||||
func (d *developmentProxy) clearTarget(address string) bool {
|
||||
target := d.target.Load()
|
||||
if target == nil || target.Host != address {
|
||||
return false
|
||||
}
|
||||
if !d.target.CompareAndSwap(target, nil) {
|
||||
return false
|
||||
}
|
||||
d.closeIdleConnections()
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *developmentProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if status, message := d.validateRequest(r); status != 0 {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
@@ -250,6 +250,26 @@ func TestWaitingPageConnectsToEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearTargetOnlyClearsSelectedUpstream(t *testing.T) {
|
||||
t.Parallel()
|
||||
proxy := newDevelopmentProxy(newEventHub())
|
||||
if err := proxy.setTarget("127.0.0.1:7001"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if proxy.clearTarget("127.0.0.1:7002") {
|
||||
t.Fatal("clearTarget cleared a different selected upstream")
|
||||
}
|
||||
if target := proxy.target.Load(); target == nil || target.Host != "127.0.0.1:7001" {
|
||||
t.Fatalf("selected upstream changed unexpectedly: %v", target)
|
||||
}
|
||||
if !proxy.clearTarget("127.0.0.1:7001") {
|
||||
t.Fatal("clearTarget did not clear the selected upstream")
|
||||
}
|
||||
if target := proxy.target.Load(); target != nil {
|
||||
t.Fatalf("selected upstream remains after clear: %v", target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevelopmentProxyRequiresLocalAuthorityAndSameOrigin(t *testing.T) {
|
||||
t.Parallel()
|
||||
var upstreamRequests atomic.Int32
|
||||
|
||||
@@ -208,6 +208,7 @@ func (s *Supervisor) Run(ctx context.Context) error {
|
||||
}()
|
||||
|
||||
var current *candidateProcess
|
||||
var currentExited <-chan struct{}
|
||||
defer func() {
|
||||
s.hub.close()
|
||||
s.proxy.closeIdleConnections()
|
||||
@@ -224,6 +225,9 @@ func (s *Supervisor) Run(ctx context.Context) error {
|
||||
s.emit(Event{Type: "ready", Phase: "proxy", Message: "http://" + listener.Addr().String()})
|
||||
if candidate := s.buildHealthyCandidate(ctx); candidate != nil {
|
||||
current = s.activateCandidate(candidate, current)
|
||||
if current != nil {
|
||||
currentExited = current.exited
|
||||
}
|
||||
}
|
||||
|
||||
roots := makeWatchRoots(s.rootDir, s.options.Config)
|
||||
@@ -242,6 +246,25 @@ func (s *Supervisor) Run(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-currentExited:
|
||||
exited := current
|
||||
current = nil
|
||||
currentExited = nil
|
||||
if exited == nil {
|
||||
continue
|
||||
}
|
||||
// Forget the selected upstream before reporting or cleanup. Future
|
||||
// requests receive the waiting page and cannot follow a reused port.
|
||||
s.proxy.clearTarget(exited.address)
|
||||
exitErr := exited.result()
|
||||
if exitErr == nil {
|
||||
exitErr = errors.New("application exited")
|
||||
} else {
|
||||
exitErr = fmt.Errorf("application exited: %w", exitErr)
|
||||
}
|
||||
_ = exited.cleanupProcessTree()
|
||||
_ = os.Remove(exited.binaryPath)
|
||||
s.report("run", exitErr)
|
||||
case err := <-serverErrors:
|
||||
if err != nil {
|
||||
return fmt.Errorf("development proxy: %w", err)
|
||||
@@ -262,22 +285,15 @@ func (s *Supervisor) Run(ctx context.Context) error {
|
||||
pending = true
|
||||
changedAt = now
|
||||
}
|
||||
if current != nil && current.hasExited() {
|
||||
exitErr := current.result()
|
||||
if exitErr == nil {
|
||||
exitErr = errors.New("application exited")
|
||||
} else {
|
||||
exitErr = fmt.Errorf("application exited: %w", exitErr)
|
||||
}
|
||||
s.report("run", exitErr)
|
||||
_ = current.cleanupProcessTree()
|
||||
_ = os.Remove(current.binaryPath)
|
||||
current = nil
|
||||
}
|
||||
if pending && now.Sub(changedAt) >= s.options.Debounce {
|
||||
pending = false
|
||||
if candidate := s.buildHealthyCandidate(ctx); candidate != nil {
|
||||
current = s.activateCandidate(candidate, current)
|
||||
if current != nil {
|
||||
currentExited = current.exited
|
||||
} else {
|
||||
currentExited = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,73 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
|
||||
waitForConnectionRefused(t, secondUpstream, 3*time.Second)
|
||||
}
|
||||
|
||||
func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("integration test builds a temporary Go application")
|
||||
}
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/himesan-dev-exit-test\n\ngo 1.25\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainPath := filepath.Join(root, "main.go")
|
||||
writeExitingTestApplication(t, mainPath, "short lived", 1500*time.Millisecond)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.ProxyAddress = "127.0.0.1:0"
|
||||
cfg.HealthPath = "/healthz"
|
||||
events := make(chan Event, 16)
|
||||
supervisor, err := New(Options{
|
||||
RootDir: root,
|
||||
Config: cfg,
|
||||
Generate: func(context.Context) error { return nil },
|
||||
OnEvent: func(event Event) { events <- event },
|
||||
CacheDir: filepath.Join(t.TempDir(), "cache"),
|
||||
PollInterval: 30 * time.Second,
|
||||
Debounce: 25 * time.Millisecond,
|
||||
BuildTimeout: 30 * time.Second,
|
||||
StartupTimeout: time.Second,
|
||||
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) }()
|
||||
t.Cleanup(cancel)
|
||||
|
||||
proxyAddress := waitForProxyAddress(t, supervisor)
|
||||
waitForBody(t, "http://"+proxyAddress+"/", "short lived")
|
||||
waitForPhase(t, events, "run")
|
||||
if target := supervisor.proxy.target.Load(); target != nil {
|
||||
t.Fatalf("proxy retained exited upstream %v", target)
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: time.Second}
|
||||
response, err := client.Get("http://" + proxyAddress + "/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, readErr := io.ReadAll(response.Body)
|
||||
_ = response.Body.Close()
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if response.StatusCode != http.StatusServiceUnavailable || !strings.Contains(string(body), "waiting for a healthy application") {
|
||||
t.Fatalf("dead upstream response = %d %q", response.StatusCode, body)
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-runResult:
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Run() did not stop after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerationFailureDoesNotMoveProxyTarget(t *testing.T) {
|
||||
t.Parallel()
|
||||
upstream := http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -173,6 +240,38 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func writeExitingTestApplication(t *testing.T, path, message string, lifetime time.Duration) {
|
||||
t.Helper()
|
||||
contents := fmt.Sprintf(`package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
time.Sleep(%d * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, "<!doctype html><html><body>%s</body></html>")
|
||||
})
|
||||
if err := http.ListenAndServe(os.Getenv("HIMESAN_LISTEN_ADDR"), mux); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
`, lifetime.Milliseconds(), message)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForPhase(t *testing.T, events <-chan Event, phase string) {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
|
||||
+13
-2
@@ -13,10 +13,16 @@ import (
|
||||
)
|
||||
|
||||
// ABI identifies the generated-code contract implemented by this version of
|
||||
// the runtime. Generated files should reference this constant so that their
|
||||
// expected ABI is visible to readers and tooling.
|
||||
// the runtime. It is descriptive metadata for people and tooling.
|
||||
const ABI = "sando.v1"
|
||||
|
||||
// ABISandoV1 is the compile-time compatibility marker for generated code that
|
||||
// requires the sando.v1 contract. A future runtime may retain this symbol while
|
||||
// it remains backward compatible; an incompatible runtime must remove it so
|
||||
// affected generated packages fail at build time instead of failing subtly at
|
||||
// render time.
|
||||
const ABISandoV1 = ABI
|
||||
|
||||
// RuntimeABI is a descriptive alias for ABI.
|
||||
const RuntimeABI = ABI
|
||||
|
||||
@@ -34,6 +40,11 @@ var (
|
||||
// Component is the complete production rendering contract. Components are
|
||||
// values rather than HTTP handlers so applications retain ownership of
|
||||
// buffering, routing, headers, status codes, and error policy.
|
||||
//
|
||||
// A Component implemented by handwritten Go is a trusted output capability: it
|
||||
// can write arbitrary bytes, block, panic, recurse, or change the surrounding
|
||||
// HTML parser context. Hime-generated components are separately checked for a
|
||||
// balanced, context-neutral HTML boundary before they implement this contract.
|
||||
type Component interface {
|
||||
Render(context.Context, io.Writer) error
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ import (
|
||||
"gamertan.com/sandwich-hime/sando"
|
||||
)
|
||||
|
||||
func TestRuntimeABIMarker(t *testing.T) {
|
||||
t.Parallel()
|
||||
if sando.ABISandoV1 != "sando.v1" || sando.ABI != sando.ABISandoV1 {
|
||||
t.Fatalf("runtime ABI=%q marker=%q", sando.ABI, sando.ABISandoV1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user