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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Security reports
url: https://github.com/tinyhumansai/rust-template/security/policy
url: https://github.com/tinyhumansai/tinysecurity/security/policy
about: Please do not report vulnerabilities through public issues.
82 changes: 71 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ env:
jobs:
rust:
name: Rust
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -54,42 +61,95 @@ jobs:
run: cargo test

# `cargo build --all-targets` only *compiles* an example. `AGENTS.md`
# promises `cargo run -p template --example basic` works, and a compiled
# promises the native loader verifier works, and a compiled
# example can still fail on its first line.
- name: Run the bundled example
run: cargo run -p template --example basic

# `crates/template-bus` exists so a host can name the payload types
if: ${{ runner.os != 'Windows' }}
shell: bash
run: |
case "$RUNNER_OS" in
Linux) module=target/debug/libtinysecurity_module.so ;;
macOS) module=target/debug/libtinysecurity_module.dylib ;;
esac
cargo run -p tinysecurity-module --example verify_module -- "$module"

- name: Verify Windows module through the restricted native copy
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$libraryName = 'tinysecurity_module'
$module = "target/debug/$libraryName.dll"
$verifyRoot = Join-Path $env:RUNNER_TEMP 'tinysecurity-module-ci-verify'
New-Item -ItemType Directory -Force $verifyRoot | Out-Null

$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$security = [System.Security.AccessControl.DirectorySecurity]::new()
$security.SetOwner($identity.User)
$security.SetAccessRuleProtection($true, $false)
$rights = [System.Security.AccessControl.FileSystemRights]::FullControl
$inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
$propagation = [System.Security.AccessControl.PropagationFlags]::None
$access = [System.Security.AccessControl.AccessControlType]::Allow
foreach ($sidValue in @(
$identity.User.Value,
'S-1-5-18',
'S-1-5-32-544'
)) {
$sid = [System.Security.Principal.SecurityIdentifier]::new($sidValue)
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$sid,
$rights,
$inheritance,
$propagation,
$access
)
[void]$security.AddAccessRule($rule)
}
Set-Acl -LiteralPath $verifyRoot -AclObject $security

$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
Copy-Item -LiteralPath $module -Destination $verifiedModule
cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule

# `crates/tinysecurity-bus` exists so a host can name the payload types
# without compiling the module. That promise is invisible in a diff,
# because a forbidden dependency arrives transitively through a feature
# someone enabled one crate away — so it is asserted rather than
# documented.
#
# The FORWARD form is required. `cargo tree -i <crate> -p template-bus`
# The FORWARD form is required. `cargo tree -i <crate> -p tinysecurity-bus`
# discards the `-p` scope, prints the whole-workspace inverse tree, and
# exits 0 looking clean even when this crate is the one at fault.
- name: Assert the contract crate stays transport-free
run: |
set -euo pipefail
forbidden="$(cargo tree -p template-bus -e normal,build --prefix none \
cargo metadata --format-version 1 --no-deps | jq -e '
[.packages[] | select(.name == "tinysecurity-bus") | .dependencies[]
| select(.kind == null) | .name] | sort == ["serde", "thiserror"]'
forbidden="$(cargo tree -p tinysecurity-bus -e normal,build --prefix none \
| grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2' || true)"
if [ -n "$forbidden" ]; then
echo "template-bus pulled in a dependency its manifest forbids:" >&2
echo "tinysecurity-bus pulled in a dependency its manifest forbids:" >&2
echo "$forbidden" >&2
echo >&2
echo "The contract is what a host compiles against. It must stay free" >&2
echo "of transports, async runtimes, HTTP clients and native libraries." >&2
exit 1
fi

# LLVM paths on Windows use native drive prefixes while Git Bash pwd
# uses /d/... . The Linux lane owns the exact per-file coverage gate;
# all three native lanes still build, test and load the real module.
- name: Require 90% line coverage in every source file
if: ${{ runner.os == 'Linux' }}
run: .github/scripts/check-file-coverage.sh 90 coverage.json

- name: Upload coverage report
if: ${{ always() }}
if: ${{ always() && runner.os == 'Linux' }}
uses: actions/upload-artifact@v7
with:
name: coverage-json
name: coverage-json-${{ matrix.os }}
path: coverage.json
if-no-files-found: ignore

Expand Down Expand Up @@ -128,7 +188,7 @@ jobs:
run: |
set -euo pipefail
msrv="$(cargo metadata --format-version 1 --no-deps \
| jq -r '.packages[] | select(.name == "template") | .rust_version')"
| jq -r '.packages[] | select(.name == "tinysecurity-module") | .rust_version')"
if [[ -z "$msrv" || "$msrv" == "null" ]]; then
echo "workspace.package.rust-version is not set in Cargo.toml" >&2
exit 1
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ permissions:

env:
# The workspace member that ships as the loadable module. Its package name is
# the artifact name and the library name; `crates/template-bus` rides along on
# the artifact name and the library name; `crates/tinysecurity-bus` rides along on
# the same inherited version and is not packaged separately.
RELEASE_PACKAGE: template
RELEASE_PACKAGE: tinysecurity-module

jobs:
prepare:
Expand Down Expand Up @@ -259,7 +259,7 @@ jobs:
macOS) module="target/release/lib${library_name}.dylib" ;;
*) echo "unsupported Unix runner: ${RUNNER_OS}" >&2; exit 1 ;;
esac
cargo run --locked --package template --example verify_module -- "$module"
cargo run --locked --package tinysecurity-module --example verify_module -- "$module"

- name: Verify Windows module through TinyBus loader
if: ${{ runner.os == 'Windows' }}
Expand All @@ -270,7 +270,7 @@ jobs:
$ErrorActionPreference = 'Stop'
$libraryName = $env:CRATE_NAME.Replace('-', '_')
$module = "target/release/$libraryName.dll"
$verifyRoot = Join-Path $env:RUNNER_TEMP 'template-module-verify'
$verifyRoot = Join-Path $env:RUNNER_TEMP 'tinysecurity-module-verify'
New-Item -ItemType Directory -Force $verifyRoot | Out-Null

$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
Expand Down Expand Up @@ -300,7 +300,7 @@ jobs:

$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
Copy-Item -LiteralPath $module -Destination $verifiedModule
cargo run --locked --package template --example verify_module -- $verifiedModule
cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule

- name: Assemble Unix module package
if: ${{ runner.os != 'Windows' }}
Expand Down Expand Up @@ -463,7 +463,7 @@ jobs:
verify_root="/opt/${CRATE_NAME}-module-verify"
install -d -m 700 "$verify_root"
install -m 755 "target/release/lib${library_name}.so" "$verify_root/"
cargo run --locked --package template --example verify_module -- \
cargo run --locked --package tinysecurity-module --example verify_module -- \
"$verify_root/lib${library_name}.so"

- name: Assemble distribution module package
Expand Down Expand Up @@ -587,5 +587,5 @@ jobs:
cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \
--package tinybus --all-features --example github_module_host -- \
"$release_url" "$archive" "$sha256"
cargo run --locked --package template --example verify_github_release -- \
cargo run --locked --package tinysecurity-module --example verify_module -- \
"$release_url" "$archive" "$sha256"
Comment thread
senamakel marked this conversation as resolved.
144 changes: 27 additions & 117 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,118 +4,23 @@ This file is the single source of truth for how humans and coding agents work
in this repository. `CLAUDE.md` is a symlink to this file, so every agent reads
the same instructions.

When you generate a new project from this template, keep this file and adapt
the project-specific parts (crate name, module map, feature flags, commands).
Delete guidance that no longer applies rather than leaving it to rot.

## Template Checklist

Do this once, in a single commit, before writing feature code:

- [ ] Rename `crates/template` and `crates/template-bus` to the project's crate
names, and update `name` in each manifest plus the `template-bus` entry in
the root `[workspace.dependencies]`.
- [ ] Set `description`, `keywords`, and `categories` in each manifest, and
`repository` in the root `[workspace.package]`.
- [ ] Rename the crate references in `README.md`, both `src/lib.rs` files,
`crates/template/examples/`, and `crates/template/tests/` (search for
`template` and `template_bus`).
- [ ] Replace the placeholder `greeting` module in both crates with the first
real feature area — payload types in the contract crate, behavior in the
module crate — keeping the `mod.rs` / `types.rs` / `test.rs` layout.
- [ ] Confirm `license` and `LICENSE` match the project's intended license.
- [ ] Update the security contact in `SECURITY.md`.
- [ ] Rename the TinyBus interface, object path, and member constants in
`crates/template-bus/src/names/`, and the matching `provides` / `methods`
declarations in `crates/template/src/tinybus_module/`, while keeping
`vendor/tinybus` pinned.
- [ ] Reset `CONTRACT_VERSION` in `crates/template-bus/src/version/` for the new
contract.
- [ ] Replace `ROADMAP.md` with the real plan, or delete it.
- [ ] Rewrite the "Project Structure" section below to describe this workspace.

## Project Structure

This is a Rust 2024 cargo workspace rooted at a virtual `Cargo.toml`. Every
crate lives under `crates/`, one directory per package, each directory named for
the package it holds. There is no root package: the crate that ships as the
loadable module is `crates/template`, the same as any other member.

```text
Cargo.toml # virtual workspace: members, [workspace.package],
# [workspace.dependencies], [workspace.lints]
crates/
├── template-bus/ # the wire contract: what crosses the bus, nothing else
│ ├── README.md # why the contract is its own crate
│ └── src/
│ ├── lib.rs # crate docs + the entire public re-export surface
│ ├── names/ # interface, object path, one constant per member
│ ├── version/ # contract version and the host bind rule
│ └── <family>/ # one directory per payload family
└── template/ # the module: behavior, adapter, and the cdylib
├── src/
│ ├── lib.rs # crate docs + public surface, re-exporting the contract
│ ├── error/mod.rs # crate-wide `Error` and `Result<T>`
│ ├── tinybus_module/ # TinyBus interface, ABI exports, integration tests
│ └── <feature>/ # one directory per feature area
│ ├── mod.rs # module docs, wiring, smallest useful public API
│ ├── types.rs # substantial type definitions
│ └── test.rs # module-local unit tests
├── tests/ # integration tests against the public API only
└── examples/ # runnable, compiled-in-CI usage examples
vendor/tinybus/ # pinned TinyBus host types and module SDK
docs/
├── specs/ # behavior and architecture specifications
├── plans/ # test-first implementation plans
└── adr/ # immutable architecture decision records
```

### The two-crate split

`crates/template-bus` holds every type that crosses the bus and the names of the
members that carry them. It has no transport, no runtime, and no behavior, and
CI asserts it stays that way. A host that only makes calls depends on it alone.

`crates/template` depends on it and re-exports all of it, so
`template::GreetRequest` and `template_bus::GreetRequest` are the *same* type
rather than structural twins. That direction is load-bearing: a parallel set of
payload types for hosts would mean a conversion at every call site that nothing
checks.

The rule for deciding where something goes: a payload type describes what a
frame carries and belongs in the contract; anything that answers a frame, holds
a connection, or touches an engine belongs in the module crate.

Add a crate by creating `crates/<name>/` — `members = ["crates/*"]` picks it up
by existing. Inherit `version`, `edition`, `rust-version`, `license`, and
`repository` from `[workspace.package]`, take shared dependencies from
`[workspace.dependencies]`, and opt into the shared lint set with:

```toml
[lints]
workspace = true
```

Each feature area belongs in a focused module directory under a crate's `src/`.
A module root explains the module, wires its pieces together, and exposes the
smallest useful API. Move substantial type definitions into `types.rs` and put
module-local unit tests in a dedicated `test.rs`, wired from the bottom of the
module root with:

```rust
#[cfg(test)]
mod test;
```
This Rust 2024 workspace ships a native TinyBus security module.

Do not accumulate inline `mod tests` blocks in implementation files, and do not
let a general-purpose `utils.rs` or `helpers.rs` grow — those are a symptom of a
missing module. Prefer many small modules that each do one thing well over few
broad ones.
- `crates/tinysecurity-bus`: versioned transport-free contract, normal dependencies serde and thiserror only.
- `crates/tinysecurity-policy`: conservative native policy engine.
- `crates/tinysecurity`: internal engine re-export, never host-linked.
- `crates/tinysecurity-module`: configured TinyBus SDK adapter and cdylib.
- `vendor/tinybus`: pinned module SDK and loader, maintained upstream.
- `docs/specs`, `docs/plans`, `docs/adr`: accepted design, ordered implementation and decisions.

Keep public exports centralized in each crate's `src/lib.rs` so downstream users
have one predictable surface. Put shared error variants in
`crates/template/src/error/mod.rs` and return the crate-wide `Result<T>` from
fallible public APIs.
Only implemented engines may advertise methods. Unknown effects deny. Module
configuration uses SDK init/reinit, not lifecycle RPCs. Hosts link only the
contract, and production migration requires released artifacts/checksums.
Tests live in sibling `*_tests.rs` files or crate `tests/` integration suites;
never use inline modules or `test.rs` filenames. Public types have rustdoc.
Do not create empty engine crates or claim full tracker completion at bootstrap.

## Build And Test

Expand All @@ -133,8 +38,8 @@ Supporting commands:

- `cargo fmt --all` — format before committing.
- `cargo test <filter>` — run a focused subset while iterating.
- `cargo test -p template-bus` — run one crate's suite.
- `cargo run -p template --example basic` — run the bundled example.
- `cargo test -p tinysecurity-bus` — run one crate's suite.
- `cargo run -p tinysecurity-module --example verify_module -- <native-library>` — run the bundled example.
- `cargo doc --no-deps --all-features` — build the rustdoc CI also builds with
`RUSTDOCFLAGS="-D warnings"`.
- `cargo test --doc` — run doctests alone when editing documentation examples.
Expand Down Expand Up @@ -185,7 +90,7 @@ add one:
- gate anything optional behind a Cargo feature, documented in `Cargo.toml`;
- declare it once in the root `[workspace.dependencies]` when more than one
crate needs it, and take it with `{ workspace = true }`;
- never add one to `crates/template-bus` that pulls in a transport, an async
- never add one to `crates/tinysecurity-bus` that pulls in a transport, an async
runtime, an HTTP client, or a native library — CI fails the build if you do;
- leave a comment above the entry explaining *why* the crate is needed and what
uses it — see the existing entries for the expected tone;
Expand All @@ -211,15 +116,20 @@ new module capability requires more.

## Testing

- Module-local unit tests live in `crates/<crate>/src/<feature>/test.rs` and may
touch private items.
- Module-local unit tests live in sibling `*_tests.rs` files and may touch
private items: use `src/<feature>/mod_tests.rs` beside `mod.rs`, or
`src/<module>_tests.rs` beside `<module>.rs`. Declare them at the bottom of
the source module with `#[cfg(test)]`, `#[path = "<module>_tests.rs"]`
(or `"mod_tests.rs"`), and `mod tests;`. The test file starts with
`use super::*;` and carries no `#[cfg(test)]` of its own. Never use inline
test modules or files named `test.rs`, `tests.rs`, or `<module>_test.rs`.
- Integration tests live in `crates/<crate>/tests/` and exercise only the public
API — they are the regression suite for the crate's contract.
- Payload types pin their serde representation in a unit test. That
representation is the wire form: a host and a module that disagree about a
field name fail at runtime with a decode error.
- Use descriptive, behavioral test names: `rejects_an_empty_name`, not
`test_greet_2`.
`test_policy_2`.
- Cover the failure paths, not just the happy path. Every new error variant
needs a test that produces it.
- For async behavior, standardize on one runtime (`tokio` as a dev-dependency
Expand All @@ -240,8 +150,8 @@ Write documentation for the reader who has never seen the code.

- Every public item gets a rustdoc comment. `missing_docs` is a warning that CI
treats as an error.
- Start every `mod.rs` and `test.rs` with a concise module-level `//!`
description.
- Start every `mod.rs` with a concise module-level `//!` description.
Sibling `*_tests.rs` files start with `use super::*;` as specified above.
- Each crate's `src/lib.rs` carries its crate-level overview: what the crate
does, the primary entry points, and a short runnable example. It should also
say what the crate deliberately does *not* hold, and why.
Expand Down Expand Up @@ -295,7 +205,7 @@ Releases run from `.github/workflows/release.yml` via a manual
an interrupted release after its version commit and tag exist. The workflow
re-runs the full validation suite, computes the next version, updates
the root `[workspace.package]` version and `Cargo.lock`, commits and tags
`vX.Y.Z`, builds `crates/template` as a TinyBus module for every supported
`vX.Y.Z`, builds `crates/tinysecurity-module` as a TinyBus module for every supported
platform, pushes, and creates an immutable GitHub release with installable
native packages.

Expand Down
Loading
Loading