9 Commits
Author SHA1 Message Date
gamertan d978994b27 Release Sandwich Hime v1 source and contribution policy
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-09-09 09:33:44 -04:00
gamertan 7e89ebfaba Publish scoped renderer measurements and reproducible samples 2026-09-09 03:20:21 -04:00
gamertan f5dcb8f10a docs: describe the installable Hime-san command 2026-09-09 02:06:48 -04:00
gamertan a9d6f0951e test: isolate release fuzz evidence from runner state 2026-09-02 20:30:44 -04:00
gamertan 3fb7fafd02 test: stabilize tagged and bounded release checks 2026-09-02 19:46:27 -04:00
gamertan e730dd1b56 Prepare Sandwich Hime v1.0.0-rc.1 source 2026-08-24 13:21:14 -04:00
gamertan efd5b997ed Prepare Sandwich Hime v1 release candidate source 2026-08-23 16:25:02 -04:00
gamertan c11552b87a policy: maintain Linux as the release target
Publishes the exact allowlisted snapshot from the private Beta 2 development line. Historical platform evidence remains truthful; native Windows and macOS are no longer release gates or support promises. Material AI assistance was reviewed by the maintainer.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-16 17:50:00 -04:00
gamertan 4fef65f9b0 docs: publish the Beta 2 evidence addendum
AI-assisted drafting and verification; maintainer review and responsibility retained.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 21:27:09 -04:00
75 changed files with 6268 additions and 457 deletions
+84 -1
View File
@@ -2,10 +2,93 @@
# Changelog # Changelog
Sandwich Hime follows semantic versioning after final v1. Compiler and nested Sandwich Hime follows semantic versioning from v1.0.0. Compiler and nested
runtime releases are versioned independently and listed together when they form runtime releases are versioned independently and listed together when they form
one coordinated release. one coordinated release.
## v1.0.0 — 2026-09-09
The coordinated compiler `v1.0.0` and runtime `sando/v1.0.0` release retains
runtime ABI `sando.v1`. Canonical Gitea tags and release announcements establish
version and artifact availability.
### Stable release contract
- Apply the v1 compatibility policy to the source syntax, generated public API,
runtime API, CLI, diagnostics, and schemas on the maintained Linux/amd64 and
Apple Silicon macOS/arm64 targets.
- Promote runtime-first installation instructions to the exact final versions.
- Retain best-effort security fixes for the latest stable v1 patch, without an
SLA, independent-audit claim, or expanded platform support promise.
### Publication tooling
- Reject references to excluded operator documentation in exported Markdown,
with positive and negative regression fixtures.
- Keep operator/archive continuity out of the public roadmap and clarify
stable public publication history, immutable version tags and separate
private development storage.
### Documentation
- Finalize the version 1.0 prospective individual CLA with recorded-license
grants, contributor safeguards, reciprocal stewardship commitments, and
explicit paired contributor/Steward acceptance for future contributions.
This does not assert that any contributor has signed, apply retroactively,
assign copyright, or require signatures to use or learn from the project.
Document commercial freedom and the compiler/runtime/application boundary
without changing the software licenses.
- Clarify Sandwich Hime as the primary identity and Hime-san as its tool name.
- Retain DCO sign-off and existing output-permission records alongside the
prospective CLA process; preserve prior contributions under their recorded
terms.
- Record the maintainer's explicit v1.0.0 deferral of offline restoration and
independent second-person recovery without representing those drills as done.
- Give the installable `himesan` command its own Go package overview covering
installation, generation, read-only checking, local development, editor
integration, and machine-readable operational contracts.
## v1.0.0-rc.1 — 2026-08-24
The first release candidate coordinates compiler `v1.0.0-rc.1` and runtime
`sando/v1.0.0-rc.1` at runtime ABI `sando.v1`. The intended v1 language,
generated API, runtime API, CLI, diagnostics, and schemas are frozen except for
release-blocking corrections.
### Added
- Maintained native Linux/amd64 and Apple Silicon macOS/arm64 verification on
pinned Go 1.26.7 and Go 1.27.0 toolchains.
- Reproducible Linux and Darwin archives with checksums, SPDX SBOMs, manifests,
build provenance, and exact public-source identities.
- A Developer ID-signed, notarized, and stapled macOS disk image for the native
arm64 compiler.
- Exact contract snapshots, contextual-escaping differentials, real-browser
compiler and development-supervisor evidence, long fuzz evidence, and
repository-owned benchmark evidence.
- Standard-library-only LSP diagnostics, hover, document symbols, component
completion, and go-to-definition retained from Beta 2 as part of the frozen
v1 command surface.
### Changed
- Linux/amd64 and Apple Silicon macOS/arm64 are the maintained execution,
verification, artifact, and release targets. WSL, native Windows, Intel
macOS, Linux/arm64, and other targets remain useful portability surfaces but
are not v1 compatibility promises.
- The module language directive remains Go 1.25 for consumer compatibility;
maintained release builds use the pinned patched Go 1.26 and Go 1.27 lines.
- Release evidence is bound to the exact sanitized public commit and tree.
GitHub remains an untagged discovery snapshot; Gitea remains the source,
contribution, module, tag, and artifact authority.
### Removed
- The native Windows PowerShell verifier and private multi-OS release-gate
workflow. Historical Windows evidence remains recorded without creating a
current support obligation.
## v1.0.0-beta.2 — 2026-08-12 ## v1.0.0-beta.2 — 2026-08-12
Compiler-only release; the unchanged Apache runtime remains Compiler-only release; the unchanged Apache runtime remains
+204 -7
View File
@@ -1,15 +1,212 @@
<!-- SPDX-License-Identifier: AGPL-3.0-only --> <!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Individual contribution agreement — counsel-review draft # Individual contribution and stewardship agreement
**This draft is not active, is not offered for signature, and creates no rights or obligations. Public pre-1.0 contributions use DCO 1.1 instead.** **Version 1.0 — prospective contribution terms.** This version takes effect for
new contribution intake when first published in the canonical public repository.
An individual agreement is formed only through the explicit contributor and
Steward acceptance in section 9. Existing contributions remain under their
recorded licenses, DCO sign-offs and output permissions; publication does not
create retroactive acceptance. Nobody must sign this agreement to learn, install,
use or build applications with Sandwich Hime.
The intended agreement preserves contributor ownership. For each contribution, the contributor represents that they have the right to submit it and grants the project and recipients a perpetual, worldwide, non-exclusive, royalty-free copyright license to use, reproduce, modify, distribute, publicly perform, publicly display, and sublicense the contribution under the license identified by the contributed file. Purpose: keep the shared project available for lawful use, study, improvement
and commercial work, without collecting contributors' ownership or giving
official stewards a private route around the project's copyleft. The practical
scope and practical limits are in
[Licensing intent](docs/LICENSING_INTENT.md).
The intended patent grant covers patent claims the contributor can license that are necessarily infringed by their contribution alone or in combination with the project as submitted. Patent litigation over the contribution terminates the patent grant to the extent counsel determines lawful and appropriate. ## 1. Parties and contribution scope
The intended agreement requires no copyright assignment. For AGPL-covered compiler contributions, it is not intended to grant a separate general right to proprietary-relicense contributor code; this does not restrict permissions already granted by a file's license, including Apache-2.0 or 0BSD. Emitted compiler scaffolding would need the narrow additional permission recorded in `OUTPUT_EXCEPTION.md`. The agreement does not promise that a contribution will be accepted or retained. The parties are the individual identified in the acceptance record ("you") and
Cole Speelman, acting in his individual capacity as maintainer of Sandwich
Hime ("the Steward"). Sandwich Hime is the project name, not a separate legal
entity. An organization does not become a party merely because you work for it.
The contributor represents that disclosed material AI assistance was reviewed by a responsible human, that the human can make the same provenance and rights representations, and that no AI system is presented as the legal contributor. A "Contribution" is material you intentionally submit for inclusion in this
repository after accepting this agreement, identified by a patch, commit or
pull request in the contribution record. Ordinary discussion, support requests,
confidential security reports, and material marked "not a contribution" are
not submissions under this agreement. Earlier work requires a separate,
explicit identification and acceptance; nothing applies retroactively by default.
If the project later considers activating a contribution agreement, qualified counsel review should address parties, definitions, governing law, notice, entity contributions, employer authorization, moral-rights treatment, patent language, and signature mechanics. Any active agreement would be announced prospectively. Until then, repository DCO sign-off is the operative contribution certification and does not activate this draft. The "Recorded License" is the file license and any explicit additional
permission identified for that Contribution at submission, with the applicable
repository revision retained. The current map is [LICENSES.md](LICENSES.md):
AGPL-3.0-only for compiler/project material and Apache-2.0 for the nested
runtime. Existing third-party material keeps its own terms. A later edit to
the map does not change a Contribution's Recorded License.
## 2. Your ownership and the public grant
You retain ownership of rights you hold. You license your Contribution to the
Steward and recipients under its Recorded License, including that license's
copyright and patent provisions, conditions, duration, termination and cure
rules. No copyright assignment, exclusive license or agency is created.
There is no additional general grant to sublicense or relicense your work
under arbitrary terms. AGPL recipients obtain their rights under the AGPL;
this agreement is not an alternate proprietary license. Permissions already
granted by Apache-2.0 or another Recorded License are not narrowed here.
You remain free to use or license rights you actually own elsewhere. You may
stop making future contributions at any time. That does not withdraw rights
already granted to recipients who comply with the applicable license. No
contribution fee, royalty, revenue share or exclusivity is required.
## 3. Generated-output permission is specific
Contributor-owned scaffolding intended to be emitted into generated output
requires your explicit `Himesan-Output-Permission: v1.0` trailer on the signed-off
submission, as described in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md). That
record identifies the affected material and retains the exact permission text
and version. This is an additional copyright permission, not an additional
patent grant.
General CLA acceptance and DCO sign-off do not supply this separate grant.
Without it, the Steward must reject or redesign the contribution so that your
unpermitted material is not emitted. The grant does not license the compiler
as a whole permissively or give rights in material you do not own.
## 4. Authority, provenance and patents
You represent that you own, or have sufficient authorization to submit and
license, the Contribution under its Recorded License. Where an employer or
another rights holder has an interest, obtain the necessary permission before
submission. If that authority is uncertain, disclose the issue and withhold
the affected material; your signature cannot grant someone else's rights.
Identify incorporated third-party material, its source and known restrictions.
Disclose material AI assistance under the revision of
[AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md) identified in your acceptance record;
a responsible human must review the submission and make the representations.
Later policy edits do not amend these contractual representations without
express agreement under section 8.
Do not send confidential prompts, personal data or secrets as provenance.
Notify the Steward if you later learn a material representation was inaccurate.
The patent grant is the one in the Recorded License, including AGPL section 11
or Apache-2.0 section 3 as applicable. This agreement adds no patent assignment,
new retaliation trigger, or guarantee that no third party holds a patent. It
does not purport to license claims outside your authority.
## 5. Steward commitments: preserve the common project
In accepting Contributions under this agreement, the Steward undertakes to:
- Publish accepted material included in an official public release under its
Recorded License, with source available as that license requires. The
agreement does not promise immediate publication of every submitted patch.
- Preserve contributor copyright notices, required attribution, license and
permission records. Public squashes must not misrepresent authorship; keep
the original signed contribution record without publishing private intake data.
- Not use this agreement to remove copyleft from contributor-owned AGPL code,
issue an exclusive or proprietary license for that code, or sell a paid
exception to its source-sharing duties. The expressly recorded output
permission remains the narrow exception already described above.
- Not seek a private side agreement to evade that official stewardship
commitment. This is a promise about the Steward's official use of accepted
material, not a restriction on a contributor's independent use of their work.
- Give no sponsor, purchaser or voting majority authority through this
agreement to acquire contributor ownership or override existing grants.
- Before voluntarily transferring official stewardship, obtain the successor's
written assumption of these stewardship obligations. Delegating maintenance
does not itself release the Steward from contractual obligations or transfer
contributor copyrights.
These are commitments by the contracting Steward, not new downstream license
conditions. They do not bind unrelated forks or non-signing third parties,
restrict permissive-license freedoms, or confer authority over rights that
another person owns. Retained public licenses remain available independently
of changes in maintainers, sponsorship or repository ownership.
## 6. Contributor and maintainer safeguards
Contributions are voluntary. Neither party promises that a patch will be
accepted, merged unchanged, maintained indefinitely or kept in every future
version. Removing a feature does not revoke licenses already granted.
No employment, partnership, governance seat, support obligation, indemnity or
obligation to fund litigation is created. Warranty disclaimers and liability
limits remain those in the Recorded License, subject to mandatory law. You
are not asked to certify worldwide freedom from infringement or waive all
claims against the Steward.
No blanket moral-rights waiver is requested. To the extent law permits, you
consent to ordinary licensed editing, combination, compilation and distribution
of the Contribution, while retaining protection against false attribution or
endorsement. Neither party may use the other's name to imply an endorsement
that was not given. This is not a power to forbid lawful criticism or forks.
The Steward may coordinate compliance reports but receives no assignment,
power of attorney or assumed standing to litigate another owner's copyright.
Separate authority may be necessary for particular enforcement action.
## 7. Commercial freedom and license limits
Users may charge for lawful services, support, distribution and applications
subject to the relevant software licenses. A complying commercial competitor
or independently branded fork does not breach this agreement merely by being
successful, and upstream contribution is not mandatory.
No customer must sign this CLA to use the software. A blanket SaaS prohibition,
competition veto, moral-use test or mandatory payment to the project is not
added to the AGPL. Network-source duties come from the applicable license;
they do not automatically reach applications built with the compiler.
## 8. Changes, disputes and succession
Changing this document or the governance policy does not amend an accepted
agreement. Any amendment requires the affected parties' express agreement to
identified new terms; continued use of the software is not acceptance.
Contract disputes do not retract lawful downstream grants or add license
termination grounds beyond the relevant software license.
The proposed governing law is Ontario law and applicable federal Canadian law,
without excluding non-waivable rights or remedies available under applicable
law. The parties should first try good-faith written resolution where practical;
no mandatory arbitration, class-action waiver or bar to urgent relief is added.
This agreement makes no guarantee about a particular court's interpretation.
## 9. Acceptance and minimal records
Identify version 1.0 using an immutable canonical commit containing this text
and the incorporated AI policy, not a moving branch link. The contributor sends
the statement below with their name, date and identified contribution through
their canonical Gitea account. A public pull-request comment is sufficient if
the contributor chooses to make it public. For private acceptance, request a
private contact channel from the maintainer before sending personal details.
Do not post employer documents, private contact details or identity documents.
> I agree to the Sandwich Hime Individual contribution and stewardship agreement
> version 1.0 and its incorporated AI policy at canonical commit [full commit],
> for contribution [pull request or commit]. I intend this statement, sent from
> my account and signed with my name and date, as my electronic acceptance.
The Steward must reply with explicit matching acceptance of the identified
agreement and contribution before merging it. Retain both statements, their
dates/account identifiers, the exact agreement and policy texts with SHA-256
digests, and the contribution's Recorded License and source identifiers.
Neither a DCO sign-off, a merge, nor silence substitutes for this exchange.
Later contributions need their own acceptance unless both parties expressly
include future intentional submissions under that exact agreement version.
Every commit still requires DCO sign-off; affected output scaffolding still
requires its separate explicit permission.
Keep private acceptance and any necessary authority records in restricted
maintainer storage, not in the public source snapshot. Do not request a home
address, government identification or unrelated employer/customer records.
The public attribution and sign-offs the contributor intentionally submits are
distinct from private intake records. The Steward uses retained records only
to administer contributions, document rights and handle related disputes, and
does not sell them or repurpose contacts for marketing.
A contributor may request access, correction or deletion through the same
private channel. Retain only records reasonably necessary to substantiate grants
still relied upon or meet applicable legal obligations; delete unnecessary
copies and explain any retention needed when responding to a request.
Withdrawing from future participation does not revoke valid existing grants.
If an individual's authority does not cover employer-owned material, do not
accept it until the rights holder's authorization is recorded. A separate
entity agreement may be needed; this document does not bind an employer by
assumption.
+15 -3
View File
@@ -8,7 +8,19 @@ Repository maintainers may temporarily disable issue or patch intake during the
pre-1.0 period; do not route around a closed intake channel by sending pre-1.0 period; do not route around a closed intake channel by sending
unsolicited private patches. unsolicited private patches.
Public pre-1.0 contributions use Developer Certificate of Origin 1.1 sign-off. The proposed `CLA.md` is an inactive draft, is not a condition of contribution, and creates no contributor or project obligations. If a contribution agreement is ever activated after legal review, the project will announce its prospective terms rather than silently applying the draft. New public contributions use DCO 1.1 sign-off and the prospective
[Individual contribution and stewardship agreement, version 1.0](CLA.md).
The agreement starts with this version's first canonical publication and binds
only people who expressly accept it; no earlier contribution is retroactively
covered. [Section 9](CLA.md#9-acceptance-and-minimal-records) gives the short
contributor statement and matching maintainer acceptance. A public Gitea
pull-request comment can record it, or contributors can request a private
channel first. Never post private identity or employer documents.
The agreement preserves ownership, the existing public licenses and reciprocal
official-steward duties. See [Licensing intent](docs/LICENSING_INTENT.md).
Learning, downloading, using Hime, and building private or commercial applications
require no contribution agreement. Ask ordinary usage questions without signing.
For local work: For local work:
@@ -20,7 +32,7 @@ go vet ./...
./scripts/check-licenses.sh ./scripts/check-licenses.sh
``` ```
Changes require focused tests, stable diagnostics, formatted generated goldens when applicable, documentation for public behavior, and a signed-off commit (`git commit -s`). The sign-off certifies the [DCO](DCO.txt); it is not a copyright assignment or acceptance of the inactive CLA. Do not commit production data, private application fixtures, secrets, build candidates, or developer cache files. Changes require focused tests, stable diagnostics, formatted generated goldens when applicable, documentation for public behavior, and a signed-off commit (`git commit -s`). The sign-off certifies the [DCO](DCO.txt); it is not a copyright assignment or a substitute for the separate agreement acceptance. Do not commit production data, private application fixtures, secrets, build candidates, or developer cache files.
The project requires no copyright assignment. Ownership remains determined by applicable law and any employer or other agreement. Contributors submit each file under the license identified for that repository area, and the DCO records their certification that they have the right to do so. Material AI assistance must follow [AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md). Review considers provenance, safety, maintenance cost, compatibility, and fit—not just whether code passes tests. The project requires no copyright assignment. Ownership remains determined by applicable law and any employer or other agreement. Contributors submit each file under the license identified for that repository area, and the DCO records their certification that they have the right to do so. Material AI assistance must follow [AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md). Review considers provenance, safety, maintenance cost, compatibility, and fit—not just whether code passes tests.
@@ -30,6 +42,6 @@ If a compiler contribution adds or changes contributor-owned scaffolding that Hi
Himesan-Output-Permission: v1.0 Himesan-Output-Permission: v1.0
``` ```
That trailer records the contributor's grant of the additional permission in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) for the affected contribution. DCO sign-off does not supply that separate grant. Maintainers must preserve the signed grant in the private contribution record even when a sanitized public snapshot uses fresh history. A patch without it must not cause contributor-owned text to be emitted; maintainers must reject or redesign such a patch rather than assume permission. That trailer records the contributor's grant of the additional permission in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) for the affected contribution. DCO sign-off does not supply that separate grant. Maintainers must preserve the signed grant in the private contribution record even when development commits are squashed into a public publication commit. Public attribution must remain accurate; squashing does not transfer ownership. A patch without it must not cause contributor-owned text to be emitted; maintainers must reject or redesign such a patch rather than assume permission.
Potential vulnerabilities follow [SECURITY.md](SECURITY.md), not the ordinary contribution channel. Do not place confidential vulnerability details in an issue or patch description. Potential vulnerabilities follow [SECURITY.md](SECURITY.md), not the ordinary contribution channel. Do not place confidential vulnerability details in an issue or patch description.
+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.
+28 -2
View File
@@ -4,10 +4,36 @@
Sandwich Hime is founder-led. Cole Speelman is lead maintainer and has final authority over product direction, merges, releases, project identity, and security coordination. Authority is exercised through reviewable public releases, written rationale for material public decisions, and the published compatibility and release policies. Publishing a prerelease creates only the support commitment stated for that version; it does not transfer stewardship. Sandwich Hime is founder-led. Cole Speelman is lead maintainer and has final authority over product direction, merges, releases, project identity, and security coordination. Authority is exercised through reviewable public releases, written rationale for material public decisions, and the published compatibility and release policies. Publishing a prerelease creates only the support commitment stated for that version; it does not transfer stewardship.
Pre-1.0 contributions use the DCO process described in `CONTRIBUTING.md`; the CLA draft is inactive. Acceptance is never guaranteed. Maintainers may close work that expands the product beyond an HTML-first, ahead-of-time Go template engine, weakens fail-closed safety, couples the runtime to a framework, or imposes unsustainable maintenance. New contributions use the DCO and prospective versioned agreement process in
`CONTRIBUTING.md`. Acceptance is never guaranteed. Maintainers may close work
that expands the product beyond an HTML-first, ahead-of-time Go template engine,
weakens fail-closed safety, couples the runtime to a framework, or imposes
unsustainable maintenance.
Maintainers disclose relevant employment or financial conflicts and recuse when a conflict would compromise review. If the project reaches three maintainers, no single employer may control a majority of seats. Maintainers disclose relevant employment or financial conflicts and recuse when a conflict would compromise review. If the project reaches three maintainers, no single employer may control a majority of seats.
Repository ownership, domain control, package namespace, release signing, and security recovery must not depend on a sponsor account. Before a supported v1.0.0 release, at least two independent humans must hold documented recovery capability. Succession may transfer official stewardship to a person or mission-aligned organization committed to the license map, public source releases, and trademark policy. Repository ownership, domain control, package namespace, release signing, and security recovery must not depend on a sponsor account. Independent recovery capability remains a governance aim; the maintainer explicitly deferred offline restoration and second-person recovery/verification for v1.0.0. They remain unproven assurance follow-ups in `ROADMAP.md`, not a claim of resilient stewardship. Succession may transfer official stewardship to a person or mission-aligned organization committed to the license map, public source releases, and trademark policy.
The project does not accept copyright assignment as a condition of contribution. It will not represent that it can proprietary-relicense contributor-owned code unless it has permission from every rights holder whose permission is required. This governance statement does not alter rights already granted by an applicable file license. The project does not accept copyright assignment as a condition of contribution. It will not represent that it can proprietary-relicense contributor-owned code unless it has permission from every rights holder whose permission is required. This governance statement does not alter rights already granted by an applicable file license.
## Shared value and independent stewardship
Official stewardship preserves the compiler's open, reciprocal licensing.
Commercial users, paid services and compliant independent forks are welcome;
funding does not purchase ownership, a governance seat, a veto, or authority to
erase attribution. Maintainers must not use a contribution agreement or private
side deal to remove copyleft from accepted contributor-owned AGPL work. The
existing runtime and output permissions remain deliberate separate boundaries.
Material licensing, governance and stewardship transfers require a public
rationale and respect for recorded rights; a vote or repository transfer cannot
supply someone else's copyright permission. A successor to a contribution
agreement must expressly assume its steward obligations. Preserve accessible
released source and provenance so lawful community continuation remains possible
without ownership of official domains, release accounts or marks. This is not a
promise of perpetual free hosting or a restriction on independent fork rights.
The reciprocal agreement requires explicit contributor and Steward acceptance.
Publication alone does not bind a contributor, an earlier submission or an
unrelated third party. No agreement is required to learn or use the software.
See [Licensing intent](docs/LICENSING_INTENT.md) for the distinctions and limits.
+9 -2
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 |
@@ -33,4 +36,8 @@ SPDX identifiers state the applicable license for comment-capable source and doc
The snapshot exporter's `PUBLIC-SNAPSHOT.json` and `PUBLIC-SNAPSHOT.sha256` are generated factual provenance records and intentionally carry no inline SPDX comment. They do not change the license of any listed file. The snapshot exporter's `PUBLIC-SNAPSHOT.json` and `PUBLIC-SNAPSHOT.sha256` are generated factual provenance records and intentionally carry no inline SPDX comment. They do not change the license of any listed file.
These are practical project licensing terms, not legal advice or a prediction of how every jurisdiction will classify a particular work. The inactive CLA draft and pre-registration trademark policy say so explicitly. Qualified legal review remains prudent before changing these terms, activating a CLA, registering marks, or making a fact-specific licensing decision; it is not represented as a prerequisite to publishing the current narrowly scoped public beta. These are practical project licensing terms, not a prediction of how every
jurisdiction will classify a particular work. The prospective contribution
agreement and pre-registration trademark policy preserve that distinction.
Professional advice may help with a specific dispute or change in circumstances;
it is not a prerequisite to this release and is not claimed to have occurred.
+1 -1
View File
@@ -1 +1 @@
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":90,"allowlist_sha256":"393ee598dc7e12cdbb603887bf06599e46b40d7c19e4ff693a818cc32afb01ec","manifest_sha256":"655bfadbddf5967288147f5b1762debd3f4b106d86c044706c41cf6aabca5e00"} {"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":128,"allowlist_sha256":"4f067a37de73082f3534ebfeb5a73b53b37ace9e4c6b21956405e2fefacbeb38","manifest_sha256":"90192f06c4ae7182834e5c1bc6890568698635b71530cb9593fb946ea2e6b3f4"}
+72 -34
View File
@@ -2,35 +2,46 @@
658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes 658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes
d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore
98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md 98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md
0828545d3aa440e1ec5dce4b934de6800413f55a925988b60923c5ee9b700a4e ./CHANGELOG.md 1b8da729e4311d8839683177ae917927d6c9e28b5e82f5d9778031b737bb7d88 ./CHANGELOG.md
b696cab3cf482ff5737501371cca749369b119351383e698ced42bcdbcbfc8ae ./CLA.md 9f0d4fd40cb2e789a6007d29fe31e00391c687b6cfd4941f3b69cbb2c5e14b03 ./CLA.md
797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md 018d21c429ef5a94b2bc5b5535086bba0b2b3849d45c5cfff3e758622a76c722 ./CONTRIBUTING.md
86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT 86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT
f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt
73cb1b092a40c56e522c5a0ebddf2b44f347cdb57bf6994cdb305d0e5697b55e ./GENERATED_CODE.md 8ac46ef4e559f0bb43a8acb8cf6013052e09d41aa591a25c5e6fd462179eb067 ./GENERATED_CODE.md
a4570d054f072d33b8f17b0c8b162a6ee0ca37d7df2b1aee7e4b728ab350a892 ./GOVERNANCE.md 2c206b42bf864ff6b2e52fa83d536f209601601c272753f4c09d1e8f44b9c8f9 ./GOVERNANCE.md
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE
47d857e49f89596bac9b09fc8ca57a668a33d01e2b51508acfc92ed321cdc27f ./LICENSES.md 4db1cfa504d3c80e06a3b3fde2854b3d01fbd2e5498d5f28d257297cd79b88c2 ./LICENSES.md
b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md 6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
c3ac873ae2e6248e1d86dd542a11557b24b8dba80e4785f3bc1018152020235c ./README.md 007ffcafd32b50ed6ba4241ced90ef2e22a79cc7ad10f768e9e1f9560453b41f ./README.md
2751674c180f15a42c1d2b40cf149be4138aa6cf247d7be176f1f0c468103c24 ./RELEASE.md fd2f45bb683d804afc127414e88e6b8ab75754af0596efe58fcb4ca3f4a55b9f ./RELEASE.md
209decb6769646eb2f58e312fbcd9c497c26234f3d3115bae3f20493b8178584 ./ROADMAP.md 953e226cb5179c17b1b2ce091653cbdaf83e92dd4913cc3b34a3cc893843496f ./ROADMAP.md
0fef473ac46b71215d1eb7922da4594210ffbbb8bb2dedd5e531fb3bd09396e1 ./SECURITY.md 44a895f8a738d21121379a5a417445ecc953f2ebfb64a7e575543769f642d1b6 ./SECURITY.md
53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md d566b8b27777cb33209602df81d654a94072ad875440c089ac41764a349b6c17 ./SPEC.md
3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md 8906258cc7aaadf03bd7e84b69efc3be6a01d1c392b0711fbcee9abf4863b25b ./TRADEMARKS.md
e2ce8cbc43e4575984839b61332207f7c90e2848fa270d4c68dbc5fc394f28e7 ./cmd/himesan-release/main.go
9dda7402f76b38d417981f9e7658880b592c282c00163a3fdbfb0ab55e1edcba ./cmd/himesan/contract_test.go
d53548ffa05a386bd869cf6b70d1ebe3397eb49bf0c05d9dc48ed94ed85a052c ./cmd/himesan/doc.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 c26f1f2030f0661aaf3bab963efc132aa63e8fae61be05ff18b4c9c8bc35cb0d ./docs/BENCHMARKS.md
5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md 015e15c7b00edd0f5d3d4906745f0a6def526cb66422aa27eb8b5c6a935d9e3d ./docs/BENCHMARK_RESULTS.md
2b815d3b815b8d338560183c6f0af46f761c2465309783c93870b8ac8d022d03 ./docs/COMPATIBILITY.md ae6043d4687708ce583e15e064c3baec2d9735b499a0f7c56a166d4bac5b18da ./docs/BRAND.md
5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md 4e9085e89db89427849509ab36a59e0e06243e65c65cb948da8f577a9d043411 ./docs/COMPATIBILITY.md
51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md d1b4ef192e02b59a9d8d0eb3b1072392e633116433fa308f6ca9bbe17b75e213 ./docs/DEVELOPMENT_SERVER.md
4d92dc88ef33d4f7d952de039f842e54a3f3bfb4dbe56debf7ddb6e6c4a428f6 ./docs/DIAGNOSTICS.md
a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md
965a6ae57a8162c3af81f4987771e88617247f903088d210720ea2afab152cc9 ./docs/SECURITY_EVIDENCE.md f66134e7a45a378e09fda4098d4fc706a280bb4ffdafe42a38a875f6ff82b3bb ./docs/LICENSING_INTENT.md
d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md c80d0ee5d2b467c696f45bac26fedc5cce04cc69148d42a3d5a103f8760da515 ./docs/SECURITY_EVIDENCE.md
738258ba8f7e5ffea67d3f00eb70839590171971a9946a55b013ca95baf7aafb ./docs/V1_RELEASE_PLAN.md e600caf17a87997de330ca787bfc5f16c1b20e07f467f8d1ad450e96e2421910 ./docs/THREAT_MODEL.md
96c5be9c958c6005c7d1fcea3843a71f501b10b5d12b545906ae1cfdd9a771db ./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 +49,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 e2843949f626c15dd6116af357588e2b471e6b03300f14f8ea76f31c1a91a2e8 ./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 +61,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 +75,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,22 +83,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 e66858232f401e112bd5f072943bc286e9a8564ad77558cdf115c023940ef318 ./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
c4a161faba46ce5b508c0788078256a520277a573a3ace0e85ae0c26b16d298b ./scripts/README.md 6ea995e9d7d22e09a13065a6e340df4efa0f1860b0b42409f84835c93f29962c ./scripts/PUBLIC_SNAPSHOT.md
0bc796f71c863aa898674a26c56f055e3d81cf20629ca7b32fbae87d8841e0a8 ./scripts/check-licenses.sh 0482438a902e1f5368af3707c1348007dc0c2efeb854be85bf4f410e6d2f4a1f ./scripts/README.md
1b003062799b99bfe271b47438397a8cce5875c60c982a0117eb11c3babcadf0 ./scripts/release-check.sh 81ab4c10a436bc9b345828a55c2fe446f297fa6cfd1debfe95e1082b6e28c969 ./scripts/check-licenses.sh
78a64c7fb3a039b15a1d08b4c0b873952852287a07f670247b081e59dbb09a30 ./scripts/verify-public-install.sh 3adba1428df83d6180d4fa3c683f0604dc7cdc0c4358f3dae7595189cc40f949 ./scripts/export-public-snapshot.sh
24ed3c9a1d37e46a856cbbd68e5c58ae04c6c9852902b99ed675e1f428339a9f ./scripts/verify.ps1 f391ac912cdeb6b139d4feaec29a992cb53faea0fc4bbe805fd380450b238f7c ./scripts/native-gate.sh
f0cbd86759fa729064cb1c69991db2ac291792dadb6b1e1ba83794f2e390404d ./scripts/verify.sh a038a002f0c895fef38a7265bcb7df01efdda57de5c4d36d0dc7c8e377798a95 ./scripts/package-macos.sh
7d1e3606f3159e0d5d222bb5b02171ad90bb2c42b0e041042dcf8e6b7692c40a ./scripts/package-native.sh
ed17f6225971af8ca9ba88dc53f307f46dcc6cb5d9f3b08d42c466ac4d0e4214 ./scripts/public-snapshot-lib.sh
4f067a37de73082f3534ebfeb5a73b53b37ace9e4c6b21956405e2fefacbeb38 ./scripts/public-snapshot.allow
8fa4ee6c6ae80c261a8f5c1ff18a89185b4ce6472ccb577e1daf61afaa146e14 ./scripts/release-check.sh
494cf5573cc6a891d4243ec01a98c43263de75090cb3b55c9c398e545b1ec1a8 ./scripts/sign-notarize-macos.sh
485f2eb4bba187e22dae820e75b4fa4ee807a1e526f21de08108c3e987390bcb ./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
+35 -27
View File
@@ -42,30 +42,38 @@ request object, or production server.
## Status ## Status
`v1.0.0-beta.2` is the current compiler beta. It is supported for This is the source and documentation for compiler `v1.0.0` and independently
learning, classroom projects, evaluation, and compatibility feedback. It is a tagged runtime `sando/v1.0.0`. The v1 source syntax, generated API, runtime
semantic-version prerelease: source syntax, generated output, the runtime API, API, CLI, diagnostics, and schemas follow the
and CLI behavior may change before final v1, and this beta is not recommended [v1 compatibility policy](docs/COMPATIBILITY.md). Corrections receive new
for production deployment. immutable versions rather than moving a published tag.
The exact Beta 1 source passed maintainer-run native Windows and executed Linux The commands below target that final pair. Canonical Gitea tags and release
matrices with Go 1.25.12 and Go 1.26.5. Native macOS validation is still announcements establish version and artifact availability; check them before
pending, so macOS support is provisional in this beta. Mac learners and Go installing a newly announced version.
developers are warmly invited to try it and share their macOS version,
architecture, Go version, command, and smallest useful reproduction. Community Linux/amd64 and Apple Silicon macOS/arm64 are the maintained v1 execution and
reports broaden the evidence; maintainers remain responsible for security release targets. Native release evidence runs with pinned Go 1.26.7 and Go
review, triage, fixes, and release decisions. 1.27.0 toolchains on both platforms; the module language directive remains Go
1.25 for consumer compatibility. WSL, native Windows, Intel macOS, and other
targets may be useful development or portability environments but are not v1
compatibility promises.
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.
Maintainers remain responsible for security review, triage, fixes, and release
decisions on both supported native targets.
Inside an application module, add the small runtime first: Inside an application module, add the small runtime first:
```sh ```sh
go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1 go get gamertan.com/sandwich-hime/sando@v1.0.0
``` ```
Then install the current beta compiler: Then install the matching compiler:
```sh ```sh
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.2 go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0
``` ```
Keep that runtime-first order. It avoids path-selection ambiguity between the Keep that runtime-first order. It avoids path-selection ambiguity between the
@@ -76,22 +84,23 @@ does not contain `sando`, seed the exact nested module without clearing the
global cache, then retry: global cache, then retry:
```sh ```sh
go mod download gamertan.com/sandwich-hime/sando@v1.0.0-beta.1 go mod download gamertan.com/sandwich-hime/sando@v1.0.0
go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1 go get gamertan.com/sandwich-hime/sando@v1.0.0
``` ```
For a reproducible one-off or classroom invocation that does not depend on the For a reproducible one-off or classroom invocation that does not depend on the
learner's `PATH`: learner's `PATH`:
```sh ```sh
go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.2 --help go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0 --help
``` ```
The unchanged runtime remains `sando/v1.0.0-beta.1`; compiler versions are The runtime implementation retains ABI `sando.v1` and zero third-party module
independent. Beta 2 adds `himesan lsp --stdio` without changing the runtime ABI. requirements. The coordinated v1 tags make the release pair explicit even
Signed compiler tags, direct fetching, the public Go proxy, and the checksum though compiler and runtime versions remain independently addressable. Signed
database are verified after publication. A newly announced version may still tags, direct fetching, the public Go proxy, and the checksum database are
need a short propagation interval before every proxy sees its immutable tag. verified after publication. A newly announced version may still need a short
propagation interval before every proxy sees its immutable tag.
For repository development: For repository development:
@@ -109,7 +118,7 @@ and serves it through `http://127.0.0.1:7331` with local-only reload
diagnostics. That is a Cole-shaped convenience, not a production server or a diagnostics. That is a Cole-shaped convenience, not a production server or a
requirement. Take the paved path—or don't. requirement. Take the paved path—or don't.
Beta 2 also provides a standard, editor-neutral language server. It analyzes Hime-san also provides a standard, editor-neutral language server. It analyzes
unsaved overlays with the compiler's real parser and context model, but never unsaved overlays with the compiler's real parser and context model, but never
generates, runs Go, executes a project, fetches a module, accesses the network, generates, runs Go, executes a project, fetches a module, accesses the network,
or starts the dev supervisor. See or starts the dev supervisor. See
@@ -117,9 +126,8 @@ or starts the dev supervisor. See
Skill and VS Code preview live in the separate Skill and VS Code preview live in the separate
[tooling repository](https://gitea.speelman.ca/gamertan/sandwich-hime-tooling). [tooling repository](https://gitea.speelman.ca/gamertan/sandwich-hime-tooling).
Final-v1 installs will use the same paths with `@v1.0.0`. A version is A version is advertised as available only after its immutable tags,
advertised as available only after its immutable tags, `gamertan.com` `gamertan.com` metadata, and clean direct-fetch installation have been verified.
metadata, and clean direct-fetch installation have been verified.
## The contract ## The contract
+70 -19
View File
@@ -21,25 +21,26 @@ 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.
Beta 1 may publish with native macOS validation pending when Windows and Linux Beta 1 and Beta 2 retain their historical platform evidence. The v1 RC line
have passed the exact-candidate matrix and macOS is clearly marked provisional. raises the maintained release gate to native Linux/amd64 and Apple Silicon
Community Mac results are valuable compatibility input; they do not transfer macOS/arm64. WSL, native Windows, Intel macOS, and other targets remain useful
security review, triage, remediation, or release responsibility away from the portability input but are not v1 release blockers or maintained promises.
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
prebuilt binaries. Signed binary artifacts, checksums, SBOMs, reproducible prebuilt binaries. Signed binary artifacts, checksums, SBOMs, reproducible
archives, and key-recovery rehearsal are mandatory for release candidates and archives are mandatory for release candidates and final v1. Offline signing-key
final v1. restoration and independent second-person recovery/verification were explicitly
deferred by the maintainer for v1.0.0. Record that assurance gap in the release
review; retain the drills as follow-up work rather than claiming they passed.
### Release candidate ### Release candidate
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 native Linux, macOS, and Windows evidence, complete release maintainer-run Linux/amd64 and native macOS/arm64 evidence, complete release
artifacts and provenance, signed tags, clean direct/proxy installs, and every RC artifacts and provenance, signed tags, clean direct/proxy installs, and every
gate in this repository. 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
@@ -49,7 +50,17 @@ published assurance gap, and the documented RC observation period. A
deployment, example, classroom project, or case study in another repository is deployment, example, classroom project, or case study in another repository is
neither imported nor required as release evidence. neither imported nor required as release evidence.
## Beta 1 publication gates For v1.0.0 the maintainer explicitly accepted the missing timed dogfooding-note
gap and deferred independent Mac installation to available alternate hardware.
Keep those dispositions visible in `ROADMAP.md`; do not recreate historical
reviews, call the install tested, or require another local profile as a
substitute. A reproduced product defect remains distinct from missing notes.
## Beta 1 publication gates (historical)
The first beta used a broader one-time platform campaign. The completed items
below are retained as publication history; they do not define future platform
support.
Before `sando/v1.0.0-beta.1` and `v1.0.0-beta.1` are created: Before `sando/v1.0.0-beta.1` and `v1.0.0-beta.1` are created:
@@ -99,8 +110,7 @@ In addition to every Beta 1 compiler/security/determinism gate:
nested-module boundaries, completion scope, and component definitions; nested-module boundaries, completion scope, and component definitions;
2. prove the language-server package does not write, execute project code, 2. prove the language-server package does not write, execute project code,
invoke Go, fetch, access the network, or start the development supervisor; invoke Go, fetch, access the network, or start the development supervisor;
3. run the exact candidate on supported Go lines under executed Linux and 3. run the exact candidate on supported Go lines under executed Linux/amd64;
native Windows, with native macOS status stated explicitly;
4. build an exact version-stamped candidate and assert the additive 4. build an exact version-stamped candidate and assert the additive
`features: ["lsp-stdio"]` JSON identity; `features: ["lsp-stdio"]` JSON identity;
5. publish a signed annotated compiler tag only after the reviewed sanitized 5. publish a signed annotated compiler tag only after the reviewed sanitized
@@ -111,21 +121,62 @@ In addition to every Beta 1 compiler/security/determinism gate:
## RC and final gates ## RC and final gates
No release candidate or v1.0.0 release occurs until every applicable gate in No release candidate or v1.0.0 release occurs until every applicable gate in
this repository is evidenced, including cross-platform deterministic this repository is evidenced, including deterministic generation on supported
generation, temporary-module compilation, fuzz/adversarial suites, Linux and Go lanes, temporary-module compilation, fuzz/adversarial suites,
race/vet/vulnerability/license checks on the latest two supported Go lines, race/vet/vulnerability/license checks on the latest two supported Go lines,
development-supervisor failure tests, and reproducible repository-owned 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 binaries, signed annotated compatible vanity-import metadata, reproducible Linux/amd64 and Darwin/arm64
tags, checksums, SBOMs, vulnerability results, and verification on Linux, binaries, signed annotated tags, checksums, SBOMs, vulnerability results, and
macOS, and Windows. 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
Gitea is the only canonical public forge. Public source is exported into a Gitea is the only canonical public forge. Public source is exported into a
separate, sanitized Gitea repository with fresh reviewed history. A sanitized separate, sanitized Gitea repository with its own reviewed history. Append
reviewed publication commits to the existing public main; do not create a new
orphan history per version or push private development branches into it. A sanitized
GitHub discovery snapshot may copy reviewed public source, but it is not an GitHub discovery snapshot may copy reviewed public source, but it is not an
issue, contribution, release, or module origin and must never receive private issue, contribution, release, or module origin and must never receive private
development refs or an indiscriminate Git mirror. Release binaries and development refs or an indiscriminate Git mirror. Release binaries and
+69 -32
View File
@@ -7,73 +7,110 @@ necessarily blockers for an earlier prerelease. The ordered initiative,
repository topology, release-candidate sequence, and definition of confidence repository topology, release-candidate sequence, and definition of confidence
are maintained in [docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md). are maintained in [docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md).
## Beta 1: public learning and evaluation ## Beta 1: public learning and evaluation (historical)
Beta 1 deliberately ships before the final-v1 compatibility and artifact gates. Beta 1 deliberately ships before the final-v1 compatibility and artifact gates.
Its scope is classroom use, learning, prototypes, and compatibility feedback; Its scope is classroom use, learning, prototypes, and compatibility feedback;
it is not a production-stability promise. it is not a production-stability promise.
- [x] Define beta versus RC/final support and compatibility policy. - [x] Define beta versus RC/final support and compatibility policy.
- [x] Establish a public pre-beta Linux/Windows matrix on Go 1.25 and Go 1.26. - [x] Establish a one-time public pre-beta Linux/Windows evidence matrix on Go
- [x] Document macOS as provisional and invite useful community reports while 1.25 and Go 1.26.
retaining maintainer responsibility for security and releases. - [x] Record the untested macOS boundary without presenting it as evidence.
- [x] Rerun all required Windows/Linux checks and deterministic generation on - [x] Rerun the historical Windows/Linux campaign and deterministic generation
the exact Beta 1 candidate. on the exact Beta 1 candidate.
- [x] Publish immutable `sando/v1.0.0-beta.1`, then - [x] Publish immutable `sando/v1.0.0-beta.1`, then
`v1.0.0-beta.1`, from the reviewed public commit. `v1.0.0-beta.1`, from the reviewed public commit.
- [x] Verify clean runtime-first direct and public-proxy installs after - [x] Verify clean runtime-first direct and public-proxy installs after
publication. publication.
- [ ] Complete native macOS maintainer validation. This is an RC/final gate,
not a Beta 1 gate.
## Compiler and runtime for RC/final ## Compiler and runtime for RC/final
- [ ] Freeze and machine-check the compiler, CLI, diagnostic, schema, generated, - [x] Freeze and machine-check the compiler, CLI, diagnostic, schema, generated,
and runtime compatibility contracts. and runtime compatibility contracts.
- [ ] Repeat compiler-owned deterministic golden output across Linux, macOS, - [x] Repeat compiler-owned deterministic golden output across the supported
and Windows on the exact candidate. Linux and macOS Go lanes on the exact candidate.
- [ ] Compile temporary consumer modules using committed Go and only the Apache - [x] Compile temporary consumer modules using committed Go and only the Apache
runtime. runtime.
- [ ] Run the parser, delimiter, context, path, and source-map release fuzz - [x] Run the parser, delimiter, context, path, and source-map release fuzz
campaign. campaign.
- [ ] Evidence adversarial escaping and filesystem cases. - [x] Evidence adversarial escaping and filesystem cases.
- [ ] Pass test, race, vet, vulnerability, and license gates on the latest two - [x] Pass test, race, vet, vulnerability, and license gates on the latest two
supported Go lines. supported Go lines.
- [ ] Reproduce signed compiler/runtime release artifacts, checksums, and SBOMs. - [x] Reproduce compiler/runtime release artifacts, checksums, and SBOMs; sign
and notarize the macOS distribution outside runner authority.
## Development supervisor for RC/final ## Development supervisor for RC/final
- [ ] Generation/build/start/health failures keep the previous healthy server - [x] Generation/build/start/health failures keep the previous healthy server
live. live.
- [ ] SSE reconnect/reload and mapped overlay diagnostics pass browser-level - [x] SSE reconnect/reload and mapped overlay diagnostics pass browser-level
tests. tests.
- [ ] CSP hash injection, fragment/API/download exclusion, and cache disabling - [x] CSP hash injection, fragment/API/download exclusion, and cache disabling
pass. pass.
- [ ] Replaced and interrupted child processes leave no descendants on - [x] Replaced and interrupted child processes leave no descendants on
supported systems. supported systems.
## Repository-owned release evidence ## Repository-owned release evidence
- [ ] Differentially test contextual escaping against Go's documented - [x] Differentially test contextual escaping against Go's documented
`html/template` safety baseline. `html/template` safety baseline.
- [ ] Reproduce repository-owned synthetic benchmark cases and methodology from - [x] Reproduce repository-owned synthetic benchmark cases and methodology from
a clean checkout. a clean checkout.
- [ ] Review generated output for stable provenance, source mappings, and - [x] Review generated output for stable provenance, source mappings, and
absence of compiler-license headers. absence of compiler-license headers.
- [ ] Document the production boundary: committed generated Go plus the Apache - [x] Document the production boundary: committed generated Go plus the Apache
runtime, with no compiler or development supervisor in the deployed binary. runtime, with no compiler or development supervisor in the deployed binary.
- [ ] Keep unsupported or unmeasured performance and production claims out of - [x] Keep unsupported or unmeasured performance and production claims out of
release materials. release materials.
## Final public launch ## Final public launch
- [ ] Complete final human review of ownership notices, output permission, DCO Final-release preparation is active. Preserve the existing reviewed public
contribution process, and pre-registration trademark terms. history and append allowlisted updates; keep operator and archival records
- [ ] Complete name clearance, security-mailbox recovery, release signing, and outside the public snapshot. Completed native and signing evidence remains
two-person credential recovery. bound to its exact candidate, not automatically to a later documentation or
- [ ] Verify `gamertan.com` vanity metadata and documented installs from clean export-tool update. Final tags and artifacts have not been published.
machines.
Use [RELEASE.md](RELEASE.md) for the remaining final-publication requirements.
Passing CI is evidence for that review, not an automatic publication decision.
- [x] Finalize the maintainer-approved individual [contribution agreement](CLA.md)
version 1.0 and explicit prospective acceptance process. Preserve the AGPL
compiler, Apache runtime, chosen application license and existing DCO/output
grants. Publication is not contributor acceptance or retroactive assent.
The maintainer chose to proceed without an outside legal-review prerequisite;
see [the rationale](docs/LICENSING_INTENT.md).
- [x] Record the maintainer's approval to proceed with ownership notices, output
permission, reciprocal contribution terms and pre-registration naming scope.
This is not a statement of external legal review or guaranteed enforceability.
- [x] Record the maintainer's decision to proceed with Sandwich Hime as the
primary project identity and Hime-san as its tool name. This is acceptance of
the documented name-review limitations, not formal trademark clearance,
completed similarity analysis or a registration requirement.
- [ ] Complete final security/signing readiness review. Distinguish successful
signing from the explicitly deferred recovery drills below.
- [x] Verify `gamertan.com` vanity metadata and documented RC installs using clean
direct-fetch and public-proxy caches. Final-version installs remain post-tag
checks; an independent Mac installation is an accepted follow-up below.
- [ ] Confirm the sanitized public Gitea source contains no private paths, - [ ] Confirm the sanitized public Gitea source contains no private paths,
identifiers, history, or unsupported claims. identifiers, history, or unsupported claims.
- [ ] Publish and observe a signed RC on every supported native platform. - [x] Publish signed RC.1 artifacts on Linux/amd64 and Darwin/arm64.
- [x] Record the maintainer's v1 acceptance of existing live dogfooding despite
missing timed checkpoint notes. This is an explicit assurance-gap acceptance,
not reconstructed reviews or a claim of measured error-free operation. Missing
notes alone do not restart the observation period or block release.
- [ ] Publish `sando/v1.0.0`, then `v1.0.0`, without moving either tag. - [ ] Publish `sando/v1.0.0`, then `v1.0.0`, without moving either tag.
## Accepted assurance follow-ups
- [ ] Exercise the published Mac download/install and CLI on independent Apple
Silicon hardware when available. The maintainer accepted this gap for v1 and
prefers an independent machine over another profile on the development Mac.
Record the result in documentation; fix any reproduced defect in an appropriate
patch release. This is not a current launch blocker or completed install test.
- [ ] Exercise offline signing-key restoration and independent second-person
credential recovery/verification. The maintainer explicitly deferred these
for v1.0.0; they are not launch blockers or completed recovery evidence. Loss
of the current credentials or operator access remains an incident-response
risk until these drills and independent access are proven.
+46 -19
View File
@@ -2,14 +2,16 @@
# Security policy # Security policy
Sandwich Hime compiler `v1.0.0-beta.2` and runtime This policy covers Sandwich Hime compiler `v1.0.0` and runtime `sando/v1.0.0`
`sando/v1.0.0-beta.1` are supported within a deliberately narrow scope: under the [v1 compatibility policy](docs/COMPATIBILITY.md). The latest published
learning, classroom projects, evaluation, and compatibility testing. stable v1 patch receives best-effort maintainer security assessment and fixes.
Security reports are welcome and receive best-effort maintainer assessment and Canonical Gitea tags and release announcements establish availability. The
fixes while this pair is current. This is not production support, published RC.1 pair remains covered until final v1 supersedes it. This policy is
an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs. not an SLA, certification, or fitness guarantee.
The community is invited to help find compatibility gaps, especially on macOS. The community is invited to help find portability gaps outside the maintained
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.
@@ -18,11 +20,14 @@ advisories, and release decisions.
| Version | Security status | | Version | Security status |
| --- | --- | | --- | --- |
| `v1.0.0-beta.2` compiler and `sando/v1.0.0-beta.1` runtime | Current evaluation/classroom prerelease pair; best-effort security assessment and fixes; interfaces may change | | `v1.0.0` compiler and `sando/v1.0.0` runtime | Stable v1 line; latest published v1 patch receives best-effort security assessment and fixes |
| `v1.0.0-rc.1` compiler and `sando/v1.0.0-rc.1` runtime | Published release-candidate pair; covered until final v1 supersedes it |
| `v1.0.0-beta.2` compiler and `sando/v1.0.0-beta.1` runtime | Superseded evaluation/classroom pair after RC publication |
| Public `main` | Development source; reports welcome, but no compatibility or production-support promise | | Public `main` | Development source; reports welcome, but no compatibility or production-support promise |
| Older prereleases | Superseded when a newer prerelease or final version is published; reports are still triaged to determine affected versions | | Older prereleases | Superseded when a newer prerelease or final version is published; reports are still triaged to determine affected versions |
Beta support lasts until it is superseded or explicitly withdrawn in the The latest stable v1 patch and current prerelease receive fixes. Prerelease
support lasts until it is superseded or explicitly withdrawn in the
supported-version table and release notes. Published tags are immutable. A supported-version table and release notes. Published tags are immutable. A
security fix is issued as a new version, never by silently replacing a tag. security fix is issued as a new version, never by silently replacing a tag.
@@ -35,7 +40,7 @@ public issue.
If that new mailbox rejects or bounces a message, retain the report and open a If that new mailbox rejects or bounces a message, retain the report and open a
canonical Gitea issue containing only the fact that the private security contact canonical Gitea issue containing only the fact that the private security contact
failed. Do not include technical details or sensitive data. The maintainer will failed. Do not include technical details or sensitive data. The maintainer will
publish a corrected private route. Ordinary usage, classroom, and macOS publish a corrected private route. Ordinary usage, classroom, and portability
compatibility reports that do not reveal a vulnerability may use a public issue. compatibility reports that do not reveal a vulnerability may use a public issue.
Helpful reports include: Helpful reports include:
@@ -55,8 +60,8 @@ ownership, backup, and recovery procedure have been tested.
## What to expect ## What to expect
These are best-effort targets for a founder-maintained project, not an SLA. These are best-effort targets for a founder-maintained project, not an SLA.
They describe maintainer responsibilities; asking the community to test a beta They describe maintainer responsibilities; inviting community testing
does not ask reporters to investigate or remediate it: does not make reporters responsible for investigation or remediation:
- acknowledge a report within 7 calendar days; - acknowledge a report within 7 calendar days;
- provide an initial severity/scope assessment within 14 calendar days when a - provide an initial severity/scope assessment within 14 calendar days when a
@@ -75,6 +80,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:
@@ -134,17 +161,17 @@ an independent security audit, certification, or formal verification. Coverage
percentages, passing scanners, and a clean vulnerability database result are percentages, passing scanners, and a clean vulnerability database result are
evidence of specific checks—not proof that no vulnerability exists. evidence of specific checks—not proof that no vulnerability exists.
The Beta 2 language server is read-only and standard-library-only. Its tests The language server is read-only and standard-library-only. Its tests
exercise bounded framing, malformed messages, cancellation, UTF-16 positions, exercise bounded framing, malformed messages, cancellation, UTF-16 positions,
unsaved overlays, workspace re-indexing, and no generated writes. Source review unsaved overlays, workspace re-indexing, and no generated writes. Source review
forbids process execution and network-client imports inside the server. Editor forbids process execution and network-client imports inside the server. Editor
workspace trust and executable resolution remain editor-client responsibilities. workspace trust and executable resolution remain editor-client responsibilities.
Beta 1 uses signed annotated source tags, but precedes the Beta 1 used signed annotated source tags but predates the complete artifact
complete prebuilt-artifact and key-recovery system. Signed binaries, checksums, system. RC.1 introduced signed or checksummed binaries, SPDX SBOMs, reproducible
an SBOM, reproducible archives, and complete source/build provenance are archives, and source/build provenance; final-v1 artifacts retain that release
release-candidate and final-v1 gates. Their absence from a source-only beta must boundary and require their own exact-version verification. Those controls
not be read as evidence of artifact authenticity beyond the verified tag and establish artifact identity and provenance; they do not constitute an
canonical source. independent security audit or prove the absence of vulnerabilities.
This policy is practical project guidance, not legal advice. This policy is practical project guidance, not legal advice.
+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.
+5
View File
@@ -4,6 +4,11 @@
“Sandwich Hime,” “Hime-san,” the Sandwich Hime flag, mascot, and official project artwork identify the project stewarded by Cole Speelman. Code licenses do not grant trademark rights, and this policy does not limit anyone's rights to use the code under those licenses. “Sandwich Hime,” “Hime-san,” the Sandwich Hime flag, mascot, and official project artwork identify the project stewarded by Cole Speelman. Code licenses do not grant trademark rights, and this policy does not limit anyone's rights to use the code under those licenses.
Sandwich Hime is the primary project identity. Hime-san names the tool within
that project, not a separate framework brand. The project does not claim the
word “Hime” in general. This naming scope is not a statement of registration
or a conclusion about confusing similarity in any jurisdiction.
Truthful, non-confusing statements such as “Built with Sandwich Hime,” “compatible with Sandwich Hime,” and unmodified community badges are welcome. They must not imply sponsorship, certification, or official status. Truthful, non-confusing statements such as “Built with Sandwich Hime,” “compatible with Sandwich Hime,” and unmodified community badges are welcome. They must not imply sponsorship, certification, or official status.
If a modified distribution uses these project marks, it should use a distinct primary product name, clearly identify its changes, and avoid official artwork or presentation that could confuse users about origin. A fork that does not use the project marks is not restricted by this policy. Either kind of fork may truthfully describe compatibility. If a modified distribution uses these project marks, it should use a distinct primary product name, clearly identify its changes, and avoid official artwork or presentation that could confuse users about origin. A fork that does not use the project marks is not restricted by this policy. Either kind of fork may truthfully describe compatibility.
+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)
}
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Command himesan is the development-time compiler and tooling entry point for
// Sandwich Hime's HTML-first, ahead-of-time .sando templates.
//
// Hime-san generates deterministic, formatted .sando.go files beside their
// sources. Applications commit those generated files and deploy their ordinary
// Go program with the small Apache-2.0 sando runtime. The compiler, language
// server, and development supervisor are not production dependencies.
//
// # Install
//
// Install the current compiler release with the Go toolchain:
//
// go install gamertan.com/sandwich-hime/cmd/himesan@latest
//
// The compiler and runtime have independent tags. For a consuming module, add
// the exact sando runtime release first, then install and pin the exact compiler
// version selected by that project's documentation. The module README records
// current release versions and the runtime-first installation sequence.
//
// # Generate and check
//
// Generate adjacent Go files for templates below the current module:
//
// himesan generate ./...
//
// Check committed output without writing files:
//
// himesan check ./...
//
// The shorter "gen" command aliases generate. The friendly "bless" command is
// a read-only alias for check. Both generate and check accept --json for one
// bounded machine-readable result.
//
// Generation is deterministic and replaces only compiler-owned output
// atomically. It does not edit handwritten Go files or go.mod. Check reports
// stale, missing, invalid, and orphaned generated output through the same
// compiler diagnostics used by generation.
//
// # Local development
//
// The optional development supervisor regenerates templates, builds the
// application's own net/http program, health-checks a new loopback candidate,
// and preserves the last healthy process when a candidate fails:
//
// himesan dev [flags] [package] [-- app-args...]
//
// Its stable local proxy and browser diagnostics are development conveniences,
// not an application framework or production server. Projects may configure
// the supervisor with himesan.json or use generate, check, and ordinary Go
// tooling directly.
//
// # Editor integration
//
// The editor-neutral language server runs over standard input and output:
//
// himesan lsp --stdio
//
// It analyzes saved sources and unsaved document overlays with the compiler's
// parser and context model. It does not generate files, execute project code,
// invoke Go, fetch modules, access the network, or start the development
// supervisor.
//
// # Operational contracts
//
// Use "himesan version --json" for compiler, runtime ABI, Go, and feature
// identity. The repository freezes the command surface, exit meanings,
// diagnostic codes, JSON schemas, and configuration schema in its contracts
// directory. Language syntax and contextual-safety rules are specified in
// SPEC.md and docs/THREAT_MODEL.md at the canonical repository.
package main
+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
}
}
}
+26
View File
@@ -2,6 +2,10 @@
# Benchmark policy # Benchmark policy
The [RC1 measurement report](BENCHMARK_RESULTS.md) records the observed values,
hardware, scope and original samples. Historical RC results must not be relabelled
as measurements of a later release candidate.
Benchmarks compare equivalent typed views and output against Go's `html/template` baseline. Reports include hardware, operating system, Go version, repository commit, dataset identity, exact commands, warmup/run counts, `ns/op`, bytes and allocations per operation, end-to-end response latency where relevant, output size, and statistical method. Benchmarks compare equivalent typed views and output against Go's `html/template` baseline. Reports include hardware, operating system, Go version, repository commit, dataset identity, exact commands, warmup/run counts, `ns/op`, bytes and allocations per operation, end-to-end response latency where relevant, output size, and statistical method.
The v1 gate is no material regression against equivalent repository-owned The v1 gate is no material regression against equivalent repository-owned
@@ -9,6 +13,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
+105
View File
@@ -0,0 +1,105 @@
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# RC1 renderer measurements
These are historical, maintainer-run measurements of the **RC1 runtime**, not
new measurements of a final v1 artifact and not an independent performance audit.
They were collected on August 24, 2026 from public commit
`e730dd1b56061501881818c7000364a55cd49e35`, tree
`931023c7d42591b5280ff81d64ce86d2ebba85b7`.
## What was measured
The committed `sando/benchmark_test.go` corpus renders one synthetic typed view
to `io.Discard` during timing. `TestBenchmarkCorpusEquivalent` first requires
identical buffered output.
`BenchmarkV1CorpusSandwichHime` mirrors generated runtime writer calls;
`BenchmarkV1CorpusHTMLTemplate` executes a pre-parsed Go standard-library template.
Both include the measured render allocations. Template parsing is not charged to
the baseline. Compiler execution, HTTP, routing, databases and deployment are
outside this measurement.
Each renderer has ten samples, with two seconds requested per sample. The result
is the median of the ten `ns/op` values, not the fastest sample. There was no
separately recorded application warmup phase; Go's benchmark harness performs
its normal iteration calibration. See the [predeclared policy](BENCHMARKS.md).
| Host | Go | Hime median ns/op | html/template median ns/op | Hime/baseline time |
| --- | --- | ---: | ---: | ---: |
| Apple M1, Darwin/arm64, 8 logical CPUs | 1.26.7 | 1,161.0 | 4,958.0 | 23.4% |
| Apple M1, Darwin/arm64, 8 logical CPUs | 1.27.0 | 1,149.5 | 4,800.5 | 23.9% |
| Xeon E5-2620 v2, Linux/amd64, 4-CPU container limit | 1.26.7 | 4,362.5 | 22,837.5 | 19.1% |
| Xeon E5-2620 v2, Linux/amd64, 4-CPU container limit | 1.27.0 | 3,877.5 | 22,003.5 | 17.6% |
All samples recorded **688 B/op and 21 allocations/op** for Sandwich Hime,
versus **1,600 B/op and 58 allocations/op** for `html/template`. Linux ran
natively in a read-only, network-disabled,
capability-dropped container limited to 4 GiB and 512 processes. macOS ran
natively. Exact OS patch/kernel versions were not captured in these benchmark
logs; do not infer them from the toolchain versions.
The baseline took approximately **4.25.7 times as long** on these lanes.
This does not establish whole-site speed, request throughput, a comparison
against every Go template engine, or a universal result on other workloads.
The samples are sequential, not a statistical study of concurrent application
load. No confidence interval or causal claim about the difference between the
two machines is implied. We do not claim to be the fastest engine.
### September 9 repeat check
A local repeat on Apple M1, Darwin/arm64, Go 1.27.0 used the same command and
runtime source unchanged from the public RC1 commit above. Output equivalence
passed. Median times were **1,034.5 ns/op**
for Hime and **4,044.0 ns/op** for the baseline (25.6%, approximately 3.9 times
as long for the baseline). Allocation counts and bytes remained 21/688 and
58/1,600 respectively. This is a local repeat, not a final-release artifact gate.
- Hime: `1028, 1021, 1028, 1031, 1033, 1097, 1063, 1065, 1041, 1036`
- Baseline: `4024, 4218, 4053, 4035, 4156, 4098, 4022, 3993, 4133, 3980`
The changed timing ratio is a useful reminder to rerun the workload on the
deployment hardware rather than treating a historical speedup as a guarantee.
## Reproduce
Check out the public commit above, select one of the recorded Go versions and,
from its `sando` directory, run:
```sh
GOWORK=off go test -run '^TestBenchmarkCorpusEquivalent$' \
-bench '^BenchmarkV1Corpus' -benchmem -benchtime=2s -count=10
```
Use `GOMAXPROCS=8` for the recorded Darwin lane or `GOMAXPROCS=4` for the Linux
lane, and document the actual hardware/resource limits. Compare within a host;
do not treat absolute times from different hardware as a controlled comparison.
## Recorded samples
Values below are `ns/op` in original sample order. Each vector contains ten
measurements. Raw-log digests identify the retained original evidence; these
vectors expose the measurements without publishing private runner metadata.
### Darwin/arm64, Go 1.26.7
- Hime: `1168, 1163, 1159, 1159, 1164, 1171, 1159, 1160, 1159, 1162`
- Baseline: `4771, 4802, 5117, 4846, 4885, 4928, 5017, 4988, 5036, 5105`
- Raw-log SHA-256: `7b68a643d685168dbd77cf3df617e8de59aa8063f6728de6c2d664173b0b05bd`
### Darwin/arm64, Go 1.27.0
- Hime: `1102, 1107, 1120, 1125, 1144, 1155, 1167, 1169, 1167, 1180`
- Baseline: `4686, 4924, 5132, 4750, 4822, 4741, 4791, 4787, 4810, 4822`
- Raw-log SHA-256: `b85481317da41bc63ad8afc317da2fd1075184405e7704a69714f42453d7dd4c`
### Linux/amd64, Go 1.26.7
- Hime: `4291, 4496, 4112, 4357, 4400, 3984, 4368, 4511, 4145, 4375`
- Baseline: `23663, 22300, 22364, 22946, 22863, 22812, 22078, 24476, 23041, 20900`
- Raw-log SHA-256: `277eee8f2488b0aa0b752584a46d3737df87defd311fd0bcf722aea81612a960`
### Linux/amd64, Go 1.27.0
- Hime: `4314, 3939, 3950, 3861, 3797, 3821, 3756, 3651, 3894, 4170`
- Baseline: `21346, 21015, 22272, 22066, 22198, 21059, 22473, 21318, 21941, 22447`
- Raw-log SHA-256: `a5d760c0fb76012b5923376dfcd80a1f1f44bf0043145f4352e4ec820aa475b3`
+4 -1
View File
@@ -2,7 +2,10 @@
# Brand vocabulary # Brand vocabulary
Use **Sandwich Hime** for the project and **Hime-san** as its friendly short form. Use `himesan` for the command, `.sando` for source, `.sando.go` for generated Go, and `sando` for the runtime. Use **Sandwich Hime** as the primary project identity and **Hime-san** for its
compiler/tool personality within that project, not as a separate framework
brand. Use `himesan` for the command, `.sando` for source, `.sando.go` for
generated Go, and `sando` for the runtime.
Never call the project bare “Hime,” which can be confused with an existing Go framework. Never use `.san`; it belongs exclusively to the separate San language project. “Sando” evokes a sandwich and keeps both projects unmistakable in editors, tooling, and search results. Never call the project bare “Hime,” which can be confused with an existing Go framework. Never use `.san`; it belongs exclusively to the separate San language project. “Sando” evokes a sandwich and keeps both projects unmistakable in editors, tooling, and search results.
+48 -29
View File
@@ -2,23 +2,20 @@
# Compatibility policy # Compatibility policy
## Beta contract ## v1 release contract
Compiler `v1.0.0-beta.2` and runtime `sando/v1.0.0-beta.1` are semantic-version This document defines the compatibility contract for compiler `v1.0.0` and
prereleases. They are supported for learning, classroom projects, evaluation, runtime `sando/v1.0.0`. Canonical Gitea tags and release announcements establish
and compatibility testing. Before final v1, source syntax, generated output, version and artifact availability. Every correction receives a new immutable
the runtime API, CLI behavior, diagnostics, and configuration may change version, documentation, and deterministic generation evidence.
without compatibility shims. Every public change must still be documented and
generation must remain deterministic.
The beta is not a production-stability commitment. Maintainers accept and Maintainers accept and triage security reports within the boundary described in
triage security reports within the boundary described in [SECURITY.md](../SECURITY.md). A security correction may intentionally fail
[SECURITY.md](../SECURITY.md), but cannot promise that a beta fix preserves its closed when retaining behavior would contradict a published safety guarantee.
public API.
## Final-v1 contract ## v1 compatibility contract
At final v1, semantic versions apply independently to the compiler and For v1, semantic versions apply independently to the compiler and
`sando` runtime. Generated files record the exact compiler version and `sando` runtime. Generated files record the exact compiler version and
required runtime ABI. Patch releases do not intentionally change accepted required runtime ABI. Patch releases do not intentionally change accepted
source semantics or generated public signatures. Minor releases may add source semantics or generated public signatures. Minor releases may add
@@ -30,13 +27,36 @@ 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 during 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. Support is based on point-in-time, The modules retain a `go 1.25` language directive for consumer compatibility.
maintainer-run release matrices, not an implication of continuous CI coverage. The maintained v1 build and verification targets are Linux/amd64 and Apple
A Go support change is announced in release notes before it takes effect. Silicon macOS/arm64 using the pinned patched Go 1.26.7 and Go 1.27.0 toolchains.
Both native targets are release blockers. A sleeping or unavailable Mac delays
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.
The current public evidence is the exact Beta 1 source at commit ### Historical Beta 1 observations
The following table is retained because the tests genuinely ran. It records a
point-in-time Beta 1 campaign and does not define the current support matrix.
The historical Beta 1 evidence is bound to source commit
`b7a84054d755e42285e50298e41e47f06a8325a5` (tree `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`be9e118e38dfebed19f60403ededdadabe07d2aa`): `be9e118e38dfebed19f60403ededdadabe07d2aa`):
@@ -45,26 +65,25 @@ The current public evidence is the exact Beta 1 source at commit
| Windows 11/amd64 on NTFS | 1.25.12, 1.26.5 | Native tests, race, vet, builds, generation, process cleanup, watcher boundaries, and temporary consumer compilation passed; privileged symlink and POSIX-only permission cases were not exercised | | Windows 11/amd64 on NTFS | 1.25.12, 1.26.5 | Native tests, race, vet, builds, generation, process cleanup, watcher boundaries, and temporary consumer compilation passed; privileged symlink and POSIX-only permission cases were not exercised |
| Linux/amd64 on WSL2 with an ext4 checkout | 1.25.12, 1.26.5 | Tests, race, vet, builds, generation, focused filesystem/development cases, and license checks passed | | Linux/amd64 on WSL2 with an ext4 checkout | 1.25.12, 1.26.5 | Tests, race, vet, builds, generation, focused filesystem/development cases, and license checks passed |
| Linux/amd64 in isolated containers on a Linux server | 1.25.12, 1.26.5 | The earlier pre-beta baseline passed tests, race, vet, builds, deterministic generation, and license checks; this was not rerun on the exact Beta 1 commit | | Linux/amd64 in isolated containers on a Linux server | 1.25.12, 1.26.5 | The earlier pre-beta baseline passed tests, race, vet, builds, deterministic generation, and license checks; this was not rerun on the exact Beta 1 commit |
| macOS | — | Native maintainer validation pending; provisional for Beta 1 | | macOS | — | Not executed during the Beta 1 campaign |
The golden generated file had SHA-256 The golden generated file had SHA-256
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f` `63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`
on every tested Windows and Linux lane. on every tested Windows and Linux lane in that historical campaign.
The signed Beta tags and fresh direct/public-proxy installation were verified The signed Beta tags and fresh direct/public-proxy installation were verified
after publication. For Beta 1, add the nested runtime to an application module after publication. For Beta 1, add the nested runtime to an application module
before installing the parent compiler at the same version; this avoids a Go before installing the parent compiler at the same version; this avoids a Go
module-cache path-selection ambiguity observed in the reverse order. module-cache path-selection ambiguity observed in the reverse order.
## macOS feedback ## Portability feedback
Mac learners, teachers, and Go developers are warmly invited to try the beta. Developers may try Hime-san on an unsupported target and report useful gaps. A
A useful compatibility report includes the macOS version, Intel or Apple good report includes the operating system and architecture, `go version`, the
Silicon architecture, `go version`, the exact command, and a minimal exact command, and a minimal reproduction or diagnostic output. Ordinary
reproduction or diagnostic output. Ordinary compatibility reports belong on portability reports belong on the canonical Gitea project. Suspected
the canonical Gitea project. Suspected vulnerabilities must use the private vulnerabilities must use the private route in [SECURITY.md](../SECURITY.md).
route in [SECURITY.md](../SECURITY.md).
Community reports can reveal gaps and help prioritize maintainer testing. They Community reports can reveal gaps and help prioritize future work. They do not
do not constitute an independent audit or shift responsibility for security constitute an independent audit, create a support promise, or shift
review, triage, fixes, and release decisions to the community. responsibility for security review, triage, fixes, and release decisions.
+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.
+115
View File
@@ -0,0 +1,115 @@
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Shared tools, commercial freedom
The design principle is **earn from useful work; preserve the shared freedoms
that made the work possible**. Commercial users are welcome. Sponsorship is
appreciated, not a license fee or a claim to project control. The intended users
are software developers and maintainers; v1 is a maintained compatibility
commitment, not a declaration that development has ended.
This document explains the [contribution agreement](../CLA.md) and official governance.
It is not a new software license, a downstream contract or a claim that every
future use or derivative of every component must be open source.
## Four different jobs
- The software license supplies recipients' permissions and obligations.
- The CLA records the rights for submitted contributions and reciprocal
promises by the named official steward; it is not a contract with every user.
- Governance determines official decisions, repository access and succession.
- The trademark policy addresses origin and endorsement, not ownership of
ideas or a veto on honest independent development.
Open-source commercial freedoms include competition. A subjective ban on
"greed," commercial hosting or businesses the maintainer dislikes would not
preserve those freedoms. We instead use concrete source-sharing, attribution,
no-hidden-relicensing and stewardship commitments. See the
[Open Source Definition](https://opensource.org/osd).
## The deliberate compiler/application boundary
The current [license map](../LICENSES.md) remains unchanged:
- Compiler/project material is AGPL-3.0-only. Distribution and covered modified
network use have source obligations under that license.
- The `sando` runtime is Apache-2.0. That is permissive, not network copyleft;
compliant closed-source derivatives are possible.
- User templates and generated applications may use their authors' chosen
terms, subject to input/dependency rights and the explicit output permission.
A proprietary paid website built with Hime is deliberately possible.
This protects the compiler's shared code without requiring its developers'
applications to become AGPL. Private production adoption is intentional: the
maintainer has confirmed this split for the release. The runtime's rendering,
escaping and URL-safety helpers are valuable implementation work; permissive
embedding makes them useful without imposing compiler licensing on applications.
It does not deliver "every part always open." Neither a later CLA nor a changed
README retracts existing recipients' licenses. No license conversion is proposed.
## Concrete cases
- A consultant sells a website built with Hime: allowed under the existing
application boundary; no mandatory payment, public badge or CLA for the client.
- A business distributes a modified compiler: it must follow the AGPL's
applicable source, licensing and notice requirements.
- A business operates a modified AGPL compiler with remote user interaction:
section 13 requires an offer of that version's Corresponding Source to those
interacting users. This is not necessarily every unrelated part of its service.
- A business merely hosts an unmodified program: hosting alone does not meet
section 13's modification condition. Wrappers and combined works require
fact-specific analysis, not an assumed universal SaaS prohibition.
- A company publishes a complying, independently branded fork: permitted,
even if it competes successfully and submits no changes upstream.
- A sponsor wants official control or private permission to close contributor
AGPL work: sponsorship grants neither; the steward covenant rejects
that official relicensing route. A fork cannot claim official endorsement.
These examples summarize boundaries, not legal opinions on a specific service.
The [AGPL text](https://opensource.org/license/agpl-3.0), including sections 2,
7, 10 and 13, controls actual covered uses. The
[Apache license](https://www.apache.org/licenses/LICENSE-2.0) controls the runtime.
The AGPL permits removing additional permissions from a redistributed copy;
do not promise that every independent fork must keep our output exception.
## What the contribution agreement adds
An identified contributor keeps ownership and grants only the recorded public
license plus an expressly authorized output permission. No alternate broad
sublicensing grant, copyright assignment, forced upstream labor, contributor
indemnity or proprietary buyout permission is collected. Patent rights and
termination follow the relevant established license.
The named Steward promises accurate attribution, preservation of contribution
records, public licensing of accepted material in official releases, no official
side deal to remove contributor copyleft, and written assumption of those duties
before a voluntary transfer of official stewardship. Those contractual promises
are not appended to downstream AGPL licenses. They cannot guarantee that a
project will never be abandoned, that a competitor will never outperform it,
or that every later actor is bound without agreement. Copyright enforcement
for contributor-owned work may require separate cooperation or authority.
Contributors remain free to license their own work elsewhere. This is reciprocal
stewardship rather than acquiring all rights from contributors. Existing public
licenses, usable source and the ability to fork preserve continuity if official
stewardship fails; governance and operational recovery still require real people.
## Deliberate adoption, without a barrier to learning
The AGPL compiler, Apache runtime and chosen application-license boundary is
settled. Version 1.0 of the contribution agreement is prospective: explicit
contributor and Steward acceptance is necessary, and no earlier contribution
is silently covered. The small recordkeeping process is in its section 9.
Nobody signs it merely to learn, download or build a private paid application.
The maintainer chose to proceed with this reviewed project-specific wording
without making outside legal review a release prerequisite. It is not an
ASF-approved agreement, a claim that counsel reviewed it, or a guaranteed
takeover shield. The [ASF ICLA](https://www.apache.org/licenses/icla.pdf) is a
useful comparison for contribution scope, authority and explicit acceptance;
its broader licensing grant is not imported into this agreement. Specific
future disputes or changes in jurisdiction may warrant professional advice.
The principle may inform other Gamertan projects, but each license, dependency
and contributor history needs its own decision. This document does not change
other repositories or turn "Canadian license" into a new license family.
+99 -22
View File
@@ -23,8 +23,46 @@ The named Windows and WSL2 platform runs used the exact public commit and tree
above. The isolated server-container matrix preceded the final candidate and above. The isolated server-container matrix preceded the final candidate and
is retained only as supplementary Linux evidence. Hostnames, network addresses, 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. Native macOS mappings are intentionally absent from this public ledger. These platform
execution remains pending and is provisional for the beta. observations are historical evidence, not the current support matrix.
Linux/amd64 and Darwin/arm64 are now the maintained v1 release targets. This
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
This addendum records the additive language-server release without replacing
the Beta 1 assessment identity above. Compiler tag `v1.0.0-beta.2` is a signed
annotated tag whose peeled public commit is
`1082d9d61eb84e67ca4012ff9ee3898ee37ac6fd` and whose public tree is
`01d5702928f3d9c9fb0e3d2213530add7ff94745`. The tag object is
`f091cd67f688ba5ee784b18f5a407a9326df7ab2`.
Beta 2 changed only the development compiler. No `sando/v1.0.0-beta.2` tag was
created. The release preflight compared the retained runtime tag
`sando/v1.0.0-beta.1` with the Beta 2 commit and verified that their `sando`
subtrees were byte-identical at tree
`3035e948f77f160d399089be3ae80c88bab3fed2`.
The exact public Beta 2 source passed the full race-enabled verifier and the
compiler-only release preflight on executed Linux with Go 1.26.5. Fresh native
Windows checkouts on NTFS passed the full race-enabled PowerShell verifier,
focused process-tree/watcher/consumer tests, candidate-stamped version checks,
and deterministic generation on Go 1.25.12 and Go 1.26.5. Clean isolated
`GOPROXY=direct` and public-proxy-only installs produced
`features:["lsp-stdio"]`; the public-proxy path also verified the retained
runtime through `sum.golang.org`. The Windows result is retained as historical
portability evidence and does not create an ongoing support promise.
The Beta 2 language server is additive development tooling. Its tested
security boundary includes protocol-only stdout; bounded header and message
framing; integer/string JSON-RPC identifiers; full-document in-memory overlays;
UTF-16 conversion at the protocol boundary; cancellation and shutdown;
workspace, nested-module, VCS, symlink, and file-count boundaries; and explicit
no-write/no-network/no-Go-tool execution tests. Fuzz targets exercise bounded
JSON-RPC framing and document changes. These checks do not make an untrusted
workspace safe to execute: `himesan dev` and project commands remain trusted
local-code operations, while `himesan lsp --stdio` performs analysis only.
## Observed security self-assessment evidence ## Observed security self-assessment evidence
@@ -45,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 | Exact-candidate native Windows and executed Linux matrices; macOS cross-compilation | Windows/Linux pass for tested lanes; native macOS pending | | 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.
@@ -58,7 +96,7 @@ and is not evidence that the executed behavior is secure.
and reachable through its analysis. A clean result cannot detect unknown flaws, and reachable through its analysis. A clean result cannot detect unknown flaws,
design errors, or vulnerabilities outside its model. design errors, or vulnerabilities outside its model.
## Beta 1 native compatibility matrix ## Historical Beta 1 compatibility matrix
These are maintainer-run, point-in-time results, not continuous CI and not an These are maintainer-run, point-in-time results, not continuous CI and not an
independent audit. independent audit.
@@ -68,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 | Native maintainer execution pending; provisional for Beta 1 | | 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`
@@ -76,12 +114,12 @@ on every tested Windows and Linux lane. Repeated generation also preserved its
timestamp. This demonstrates cross-host agreement for one compiler-owned timestamp. This demonstrates cross-host agreement for one compiler-owned
fixture, not equivalence for every possible template. fixture, not equivalence for every possible template.
Mac learners and Go developers are warmly invited to report ordinary Portability reports for unsupported targets may include the operating system,
compatibility results with macOS version, architecture, `go version`, exact architecture, `go version`, exact command, and a minimal reproduction.
command, and a minimal reproduction. Suspected vulnerabilities use the private Suspected vulnerabilities use the private route in
route in [SECURITY.md](../SECURITY.md). Community reports help find gaps; [SECURITY.md](../SECURITY.md). Such reports help find gaps but do not create a
maintainers remain responsible for reproducing security-relevant behavior, support promise; maintainers remain responsible for security triage and fixes
triage, remediation, and release decisions. on both supported native targets.
## Security-relevant design evidence ## Security-relevant design evidence
@@ -146,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
@@ -169,10 +212,17 @@ known-vulnerability scans, candidate-version provenance checks, native Windows
and executed Linux matrices, and Windows/macOS cross-compilation on 2026-08-12. 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. Native resolution and the public Go proxy after normal proxy propagation. Future
macOS and the other gaps below remain separate release decisions. release decisions require fresh evidence for the maintained Linux/amd64 and
Darwin/arm64 targets rather than reusing this historical campaign.
## Open assurance gaps ## Historical pre-RC assurance gaps
The list below preserves the pre-RC assessment, not current completion status.
RC.1 native verification and artifact publication are recorded in
`../CHANGELOG.md`; the final-release queue is `../ROADMAP.md`. The v1
dispositions below distinguish required exact-candidate evidence from explicitly
accepted recovery deferrals.
- delivery to `security@sandwichhime.com` is owner-confirmed through a - delivery to `security@sandwichhime.com` is owner-confirmed through a
controlled domain catch-all; encrypted reporting, documented backup, and controlled domain catch-all; encrypted reporting, documented backup, and
@@ -180,10 +230,10 @@ macOS and the other gaps below remain separate release decisions.
- 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;
- native macOS, Linux/arm64, and Windows/arm64 execution remain outstanding; - Linux/arm64, Darwin/amd64, Windows, and other targets are outside the current
- Windows symlink rejection was not natively exercised because the test account maintained release set;
lacked symlink privilege; - the exact public candidate still needs the committed real-browser generated
- browser-parser differential and semantic property testing need expansion; 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;
@@ -197,6 +247,33 @@ macOS and the other gaps below remain separate release decisions.
- 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 | Delivery/reply remains required and is maintainer-confirmed. Offline restoration and independent second-person recovery/verification are explicitly deferred assurance work for v1.0.0, not completed drills. Encrypted reporting may remain optional if the supported confidential channel and its limit are stated accurately. |
| Artifact signing, provenance, and key recovery | Deterministic native artifacts, Developer ID notarization and signed-tag evidence remain required for the exact release candidate. Offline signing-key restoration and independent second-person recovery/verification are explicitly deferred for v1.0.0; successful signing does not prove recovery. |
| 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 native Windows/macOS security and process-lifecycle 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
+81 -55
View File
@@ -16,29 +16,40 @@ releases originate only from the reviewed canonical public tree.
| Surface | Purpose | History and tags | | Surface | Purpose | History and tags |
| --- | --- | --- | | --- | --- | --- |
| Private development storage | Working branches, private review records, and historical context | identities and history are not exported; no public release tags | | Private development storage | Working branches, private review records, and historical context | identities and history are not exported; no public release tags |
| Public Gitea `sandwich-hime` | Canonical sanitized source, contribution venue, module origin, and releases | fresh reviewed history; authoritative immutable `sando/vX.Y.Z` and `vX.Y.Z` tags | | Public Gitea `sandwich-hime` | Canonical sanitized source, contribution venue, module origin, and releases | stable reviewed public history; authoritative immutable `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 | | 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, Each public update is exported through the exact committed allowlist, inspected,
committed as a fresh public snapshot, and compared byte-for-byte with the appended as a meaningful publication commit to the existing public `main`, and
reviewed export. GitHub receives that public tree only. It never receives compared byte-for-byte with the reviewed export. Development commits may be
private history or an indiscriminate Git mirror. squashed for publication; do not reset public ancestry for each version, merge
private history into public branches, or force-push existing releases. Versions
are immutable tags on that public history, not separate orphan branches.
Preserve contributor attribution, sign-offs and any output-permission grants
as required by [CONTRIBUTING.md](../CONTRIBUTING.md).
GitHub receives the reviewed public tree only. It never receives private
branches, private notes or an indiscriminate Git mirror. Branch names and
protected branches are not privacy boundaries: private development remains in
a separate access-controlled repository. The tutorial, tooling and website
remain independent projects, not archival branches of the compiler.
## Current readiness ## Current readiness
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, native-platform, artifact, signing, or soak gates are v1 compatibility, native release, artifact, signing, or soak gates are complete.
complete.
### Demonstrated for Beta 1 ### Demonstrated for Beta 1
Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`be9e118e38dfebed19f60403ededdadabe07d2aa`) passed maintainer-run Go `be9e118e38dfebed19f60403ededdadabe07d2aa`) passed maintainer-run Go
1.25.12 and Go 1.26.5 matrices on native Windows/amd64, Linux/amd64 under WSL2, 1.25.12 and Go 1.26.5 matrices on native Windows/amd64 and Linux/amd64 under
with the earlier pre-beta server-container run retained only as supplementary WSL2, with the earlier pre-beta server-container run retained only as
Linux evidence. The same generated golden SHA-256 was observed across the exact supplementary Linux evidence. The same generated golden SHA-256 was observed
Beta Windows and Linux lanes. across the exact Beta Windows and Linux lanes. This is historical evidence,
not the current support definition; Linux/amd64 and Darwin/arm64 are the
maintained v1 release targets.
Other demonstrated controls include: Other demonstrated controls include:
@@ -50,17 +61,25 @@ Other demonstrated controls include:
- loopback-only, browser-origin-hardened development proxy behavior; and - loopback-only, browser-origin-hardened development proxy behavior; and
- a public threat model, security policy, and dated evidence ledger. - a public threat model, security policy, and dated evidence ledger.
### Not demonstrated yet ### Demonstrated for the first RC candidate
- native maintainer-run macOS execution; macOS is provisional for Beta 1; - native Linux/amd64 and Darwin/arm64 release-candidate lanes on pinned Go
- stable final-v1 API, CLI, schema, diagnostic, and generated snapshots; 1.26.7 and Go 1.27.0 toolchains;
- systematic browser-parser and `html/template` differential testing; - exact-public-candidate execution of the committed real-browser generated
document and development-supervisor gates;
- 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;
- complete real-browser development-supervisor evidence; - deterministic prebuilt archives, checksums, SPDX SBOMs, and provenance;
- deterministic prebuilt archives, checksums, SBOMs, signed binaries, and - Developer ID signing, notarization, stapling, and Gatekeeper validation for
tested signing/recovery procedures; or the Darwin/arm64 distribution; and
- native macOS installation of the published Beta 1 tags. - byte-identical generated output and independently reproduced release
archives across the maintained native evidence lanes.
The final immutable tags, clean direct/proxy installation and public observation
review remain publication or post-publication gates rather than properties
that source code can prove by itself. Offline restoration and second-person
recovery/verification are explicitly deferred assurance work for v1.0.0, as
recorded in [ROADMAP.md](../ROADMAP.md).
## Beta 1 publication lane ## Beta 1 publication lane
@@ -68,9 +87,8 @@ Beta 1 is supported for learning, classroom projects, evaluation, prototypes,
and compatibility feedback. It is not recommended as a production-stable and compatibility feedback. It is not recommended as a production-stable
dependency, and its interfaces may change. dependency, and its interfaces may change.
- [x] Define beta support, security, compatibility, and macOS-provisional - [x] Define beta support, security, and compatibility language.
language. - [x] Establish the historical public pre-beta Linux/Windows evidence baseline.
- [x] Establish the named public pre-beta Linux/Windows baseline.
- [x] Rerun the supported Go matrix and deterministic generation on the exact - [x] Rerun the supported Go matrix and deterministic generation on the exact
Beta 1 candidate. Beta 1 candidate.
- [x] Run the candidate-version freshness, bounded fuzz, vulnerability, and - [x] Run the candidate-version freshness, bounded fuzz, vulnerability, and
@@ -79,64 +97,68 @@ dependency, and its interfaces may change.
`v1.0.0-beta.1`, from the same reviewed public commit. `v1.0.0-beta.1`, from the same reviewed public commit.
- [x] Verify clean runtime-first direct and public-proxy installs and record the - [x] Verify clean runtime-first direct and public-proxy installs and record the
result. result.
- [ ] Add native macOS maintainer evidence before RC; community reports inform
that work but do not replace maintainer responsibility.
## Milestone 1: contract freeze ## Milestone 1: contract freeze
Required before security/platform 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 native-platform evidence ## Milestone 2: security and native release evidence
- [ ] Run the minimum supported Go line and the latest two stable Go lines on - [x] Run the pinned patched Go 1.26 and Go 1.27 lines on Linux/amd64 and
native Linux, macOS, and Windows hosts. native Darwin/arm64 runners, plus a Linux deployment-class host.
- [ ] Prove identical generated bytes across those hosts and exercise native - [x] 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
- [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports, Chrome rather than relying only on byte equality.
- [x] 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 - [x] Measure the exact release candidate and preserve 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.
- [x] 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.
## Milestone 4: release rehearsal ## Milestone 4: release rehearsal
- [ ] Make version validation identical in the CLI, generated headers, scripts, - [x] Make version validation identical in the CLI, generated headers, scripts,
and release artifacts; reject ambiguous build metadata. and release artifacts; reject ambiguous build metadata.
- [ ] Build the candidate compiler at its candidate version and prove its - [x] Build the candidate compiler at its candidate version and prove its
committed outputs are current under that exact binary. committed outputs are current under that exact binary.
- [ ] Produce deterministic archives/binaries, checksums, SBOMs, signatures, - [x] Produce deterministic archives/binaries, checksums, SBOMs, signatures,
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, - [x] Record the explicit v1.0.0 deferral of offline release-key restoration and
signing material, and publication instructions. independent second-person recovery; retain the drills as assurance follow-ups
- [ ] Make evidence gates validate content and commit identity rather than only in [ROADMAP.md](../ROADMAP.md), without claiming they passed.
- [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.
@@ -147,12 +169,16 @@ Required before security/platform 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 and native platforms. `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,
or rollback regression. This is product dogfooding, not a dependency on or rollback regression. This is product dogfooding, not a dependency on
another application's private repository. another application's private repository. For v1.0.0 the maintainer accepted
the existing live-use experience despite missing timed checkpoint notes;
see [ROADMAP.md](../ROADMAP.md). This does not assert the missing reviews
occurred or waive a reproduced Hime defect.
6. Fix findings in a new RC; restart the observation period when the affected 6. Fix findings in a new RC; restart the observation period when the affected
boundary warrants it. boundary warrants it.
7. Finalize the changelog, supported-version table, migration notes, release 7. Finalize the changelog, supported-version table, migration notes, release
@@ -172,6 +198,6 @@ marketing. New features do not outrank a small stable contract.
## Definition of confidence ## Definition of confidence
“Ready for v1” means a reviewer can trace each promise to a stable public “Ready for v1” means a reviewer can trace each promise to a stable public
contract, executable evidence from supported native environments, and a signed contract, executable evidence from supported Linux environments, and a signed
artifact built from the exact canonical source. It does not mean perfect, artifact built from the exact canonical source. It does not mean perfect,
invulnerable, or finished forever. invulnerable, or finished forever.
+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)
}
}
+11 -2
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)
} }
@@ -163,7 +172,7 @@ func buildHimesanBinary(t *testing.T, repositoryRoot, name, linkerFlags string)
name += ".exe" name += ".exe"
} }
path := filepath.Join(t.TempDir(), name) path := filepath.Join(t.TempDir(), name)
arguments := []string{"build", "-trimpath"} arguments := []string{"build", "-buildvcs=false", "-trimpath"}
if linkerFlags != "" { if linkerFlags != "" {
arguments = append(arguments, "-ldflags", linkerFlags) arguments = append(arguments, "-ldflags", linkerFlags)
} }
+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)
}
}
+21
View File
@@ -4,6 +4,24 @@
`gamertan.com/sandwich-hime/sando` is the independent Apache-2.0 runtime ABI for Go generated by Sandwich Hime. It contains no compiler, router, HTTP server, middleware, reflection registry, project discovery, or development supervisor. `gamertan.com/sandwich-hime/sando` is the independent Apache-2.0 runtime ABI for Go generated by Sandwich Hime. It contains no compiler, router, HTTP server, middleware, reflection registry, project discovery, or development supervisor.
## Install
This is the source and documentation for runtime `sando/v1.0.0`, paired with
compiler `v1.0.0` without changing runtime ABI `sando.v1`. Canonical Gitea tags
and release announcements establish availability. In an application module,
add the runtime first, then install the compiler:
```sh
go get gamertan.com/sandwich-hime/sando@v1.0.0
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0
```
The runtime-first order avoids parent/nested-module path-selection ambiguity.
The compiler is a development tool and is not a runtime dependency of the
generated application. Compiler and runtime remain independently versioned.
## API and licensing
The stable surface is: The stable surface is:
```go ```go
@@ -16,4 +34,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.
+32 -5
View File
@@ -2,20 +2,47 @@
# Repository verification tools # Repository verification tools
These scripts are intentionally understandable shell and PowerShell rather than a release framework with hidden defaults. These scripts are intentionally understandable shell rather than a release
framework with hidden defaults. The maintained native verification paths are
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.
- `verify.ps1` provides the equivalent native Windows lane; pass `-Race` to include the race detector.
- `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 export scanner also rejects Markdown references to known excluded operator
documentation roots. It is a bounded publication check, not a complete Markdown
link checker or a substitute for inspecting the exported diff. Source-code
fixtures can still describe the scanner's rejected paths.
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 snapshot until the project has confirmed its own Gitea runner availability and reviewed locally hosted or otherwise pinned dependencies. Local `verify.sh`, `verify.ps1`, license, and release-preflight results are the preview gates. Forge workflows are intentionally excluded from the sanitized pre-1.0 public
snapshot. The private development repository uses pinned Linux/amd64 and
repository-scoped native macOS/arm64 runners; the public source remains
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"
+76
View File
@@ -0,0 +1,76 @@
#!/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
(
fuzz_cache=$(mktemp -d "${TMPDIR:-/tmp}/himesan-fuzz-cache.XXXXXXXX")
trap 'rm -rf -- "$fuzz_cache"' EXIT HUP INT TERM
export GOCACHE="$fuzz_cache"
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=15s -parallel=1
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=15s -parallel=1
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=15s -parallel=1
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=15s -parallel=1
(
cd sando
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=15s -parallel=1
)
)
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'
+132
View File
@@ -0,0 +1,132 @@
#!/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'
# Known excluded documentation roots, not ordinary prose about private work.
# Scope this to Markdown: exported scanner code legitimately names its policy.
local private_markdown_ref='(^|[^[:alnum:]_./%-])([.]{1,2}/)*(private|history)/'
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
case $rel in
*.[mM][dD] | *.[mM][aA][rR][kK][dD][oO][wW][nN])
if LC_ALL=C grep -Eiq -- "$private_markdown_ref" "$file"; then
echo "public snapshot: excluded documentation reference in $rel" >&2
return 1
fi
;;
esac
done < <(find "$root" -type f -print0 | LC_ALL=C sort -z)
}
+132
View File
@@ -0,0 +1,132 @@
# 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/doc.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/BENCHMARK_RESULTS.md
docs/BRAND.md
docs/COMPATIBILITY.md
docs/DEVELOPMENT_SERVER.md
docs/DIAGNOSTICS.md
docs/LANGUAGE_SERVER.md
docs/LICENSING_INTENT.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
+43 -36
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,13 +241,22 @@ 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'
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s (
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s export GOCACHE="$artifact_dir/fuzz-go-build-cache"
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s mkdir -p "$GOCACHE"
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s -parallel=1
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s -parallel=1
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s -parallel=1
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s -parallel=1
(
cd sando
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=20s -parallel=1
)
)
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,24 +265,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==> cross-compiling release binary smoke set\n' target="$(go env GOOS)/$(go env GOARCH)"
for target in \ case "$target" in
linux/amd64 \ linux/amd64 | darwin/arm64) ;;
linux/arm64 \ *) printf 'error: release preflight requires a maintained native target, found %s\n' "$target" >&2; exit 1 ;;
darwin/amd64 \ esac
darwin/arm64 \ target_os=${target%/*}
windows/amd64 \ target_arch=${target#*/}
windows/arm64; do printf '\n==> building supported native %s release binary\n' "$target"
target_os=${target%/*} CGO_ENABLED=0 go build -trimpath -ldflags "$compiler_linker_flags" \
target_arch=${target#*/} -o "$artifact_dir/himesan-$target_os-$target_arch" ./cmd/himesan
extension=''
if [[ "$target_os" == windows ]]; then
extension='.exe'
fi
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" \
go build -trimpath -ldflags "$compiler_linker_flags" \
-o "$artifact_dir/himesan-$target_os-$target_arch$extension" ./cmd/himesan
done
for required in \ for required in \
scripts/verify-public-install.sh \ scripts/verify-public-install.sh \
@@ -283,23 +287,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
cross-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"
+305
View File
@@ -0,0 +1,305 @@
#!/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"
# Public prose and ordinary public links are allowed. Source-code policy
# fixtures may name excluded roots without becoming documentation links.
printf '%s\n' 'Private development records remain separate.' \
'[Releases](docs/releases.md)' '[History](https://example.invalid/history/releases)' \
'[Design](private-design.md)' >"$safe_tree/README.md"
printf '%s\n' '// Reject private/notes.md in an export policy.' >"$safe_tree/scanner_test.go"
snapshot_validate_export_tree "$safe_tree"
markdown_tree=$temporary/markdown
mkdir -p "$markdown_tree"
for reference in '[Audit](private/audit.md)' '[Audit](./private/audit.md#resume)' \
'[Audit](../../private/audit.md)' '[audit]: ../private/audit.md' '[audit]:private/audit.md' \
'`private/audit.md`' '<a href="private/audit.md">Audit</a>' \
'[Archive](history/story.md)' '[Audit](PRIVATE/AUDIT.md)'; do
printf '%s\n' "$reference" >"$markdown_tree/README.Md"
if snapshot_validate_export_tree "$markdown_tree" >"$temporary/markdown.log" 2>&1; then
echo "excluded documentation reference was accepted: $reference" >&2
exit 1
fi
grep -q 'excluded documentation reference in README.Md' "$temporary/markdown.log"
done
mv "$markdown_tree/README.Md" "$markdown_tree/notes.markdown"
if snapshot_validate_export_tree "$markdown_tree" >/dev/null 2>&1; then
echo "excluded reference in .markdown documentation was accepted" >&2
exit 1
fi
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
-133
View File
@@ -1,133 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-only
[CmdletBinding()]
param(
[switch]$Race
)
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
Set-Location $RepoRoot
function Invoke-Checked {
param(
[Parameter(Mandatory = $true)]
[string]$Label,
[Parameter(Mandatory = $true)]
[scriptblock]$Command
)
Write-Host "`n==> $Label"
& $Command
if ($LASTEXITCODE -ne 0) {
throw "$Label failed with exit code $LASTEXITCODE"
}
}
function Invoke-ModuleChecks {
param(
[Parameter(Mandatory = $true)]
[string]$Directory,
[Parameter(Mandatory = $true)]
[string]$Label
)
Push-Location $Directory
try {
Invoke-Checked "$Label`: go test" { go test ./... }
Invoke-Checked "$Label`: go vet" { go vet ./... }
}
finally {
Pop-Location
}
}
function Get-SandoSources {
if (-not (Test-Path "internal/compiler/testdata/golden" -PathType Container)) {
return @()
}
return @(Get-ChildItem "internal/compiler/testdata/golden" -File -Filter "*.sando" |
Sort-Object FullName)
}
function Get-GeneratedManifest {
$lines = foreach ($source in (Get-SandoSources)) {
$output = "$($source.FullName).go"
if (-not (Test-Path $output -PathType Leaf)) {
"missing $output"
continue
}
$hash = (Get-FileHash -Algorithm SHA256 $output).Hash.ToLowerInvariant()
$modified = (Get-Item -LiteralPath $output).LastWriteTimeUtc.Ticks
"$hash $modified $output"
}
return ($lines -join "`n")
}
$TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("himesan-verify-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $TempRoot | Out-Null
try {
Invoke-ModuleChecks "." "compiler module"
Invoke-Checked "compiler module: go build" {
go build -trimpath -o (Join-Path $TempRoot "himesan.exe") ./cmd/himesan
}
if (-not (Test-Path "sando/go.mod" -PathType Leaf)) {
throw "nested Apache runtime module sando/go.mod is missing"
}
Invoke-ModuleChecks "sando" "sando runtime module"
$Sources = @(Get-SandoSources)
if ($Sources.Count -eq 0) {
throw "compiler-owned golden .sando fixture is missing"
}
else {
$SourcePaths = @($Sources | ForEach-Object { $_.FullName })
$CheckArgs = @("run", "./cmd/himesan", "check") + $SourcePaths
$GenerateArgs = @("run", "./cmd/himesan", "generate") + $SourcePaths
Invoke-Checked "golden generation: read-only freshness check" {
& go $CheckArgs
}
$Before = Get-GeneratedManifest
Invoke-Checked "golden generation: first deterministic pass" {
& go $GenerateArgs
}
$First = Get-GeneratedManifest
if ($Before -cne $First) {
throw "generation changed committed output after check declared it fresh"
}
Invoke-Checked "golden generation: second deterministic pass" {
& go $GenerateArgs
}
$Second = Get-GeneratedManifest
if ($First -cne $Second) {
throw "repeated generation changed output bytes or an unchanged timestamp"
}
Invoke-Checked "golden generation: final freshness check" {
& go $CheckArgs
}
}
if ($Race) {
Invoke-Checked "compiler module: race tests" { go test -race ./... }
Push-Location "sando"
try {
Invoke-Checked "sando runtime module: race tests" { go test -race ./... }
}
finally {
Pop-Location
}
}
Write-Host "`n==> verification complete"
}
finally {
if (Test-Path $TempRoot -PathType Container) {
Remove-Item -LiteralPath $TempRoot -Recurse -Force
}
}
+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"