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
108 changes: 108 additions & 0 deletions .github/workflows/app-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: App contract

# crates/app-contract (octosense-app-contract, OctoSense ADR 0005): the crate
# apps and hosts depend on by version. It is checked on its own, the way
# crates.io builds it: copied out of the workspace, whose [patch] tables name
# sibling checkouts (../makepad, ../octoscript…) the contract does not use.
#
# test the unit tests, the fixture corpus, the doc tests, clippy and a
# publish dry run (no git or path dependency can slip in)
# semver cargo semver-checks against the latest published 1.x: within 1.x
# the contract only grows, so a non-additive change fails here and
# not in an app. Skipped, with a notice, until 1.0.0 is published.

on:
push:
branches: [main]
paths:
- 'crates/app-contract/**'
- '.github/workflows/app-contract.yml'
pull_request:
paths:
- 'crates/app-contract/**'
- '.github/workflows/app-contract.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ format('app-contract-{0}', github.ref) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
CARGO_TERM_COLOR: always
CRATE: octosense-app-contract

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Copy the crate out of the workspace
shell: bash
run: cp -R crates/app-contract "$RUNNER_TEMP/app-contract"
- name: Tests and the fixture corpus
working-directory: ${{ runner.temp }}/app-contract
run: cargo test --all-targets && cargo test --doc
- name: Clippy
working-directory: ${{ runner.temp }}/app-contract
run: cargo clippy --all-targets --no-deps -- -D warnings
- name: Package and publish dry run
working-directory: ${{ runner.temp }}/app-contract
run: |
cargo package --list --allow-dirty
cargo publish --dry-run --allow-dirty

semver:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Find the latest published version
id: published
shell: bash
run: |
set -euo pipefail
# crates.io's sparse index: one JSON line per published version.
status=$(curl --proto '=https' --tlsv1.2 --silent --show-error --location \
--output "$RUNNER_TEMP/index" --write-out '%{http_code}' \
"https://index.crates.io/oc/to/$CRATE")
case "$status" in
200)
latest=$(jq -r 'select(.yanked | not) | .vers' "$RUNNER_TEMP/index" | sort -V | tail -n 1)
echo "latest=$latest" >> "$GITHUB_OUTPUT"
echo "Diffing the public API against $CRATE $latest."
;;
404)
echo "latest=" >> "$GITHUB_OUTPUT"
echo "::notice::$CRATE is not published on crates.io yet, so there is no version to diff against: skipping cargo semver-checks. The check runs from the first published release on."
;;
*)
echo "::error::crates.io index answered HTTP $status for $CRATE; cannot tell whether a version is published."
exit 1
;;
esac
- name: Install cargo-semver-checks
if: steps.published.outputs.latest != ''
uses: taiki-e/install-action@v2
with:
tool: cargo-semver-checks
- name: API diff (cargo semver-checks)
if: steps.published.outputs.latest != ''
shell: bash
env:
BASELINE: ${{ steps.published.outputs.latest }}
run: |
set -euo pipefail
cp -R crates/app-contract "$RUNNER_TEMP/app-contract"
cd "$RUNNER_TEMP/app-contract"
if ! cargo semver-checks check-release --manifest-path Cargo.toml --baseline-version "$BASELINE"; then
echo "::error::octosense-app-contract changed its public API in a way 1.x does not allow (ADR 0005 section 2). Make the change additive, or bump the version deliberately if this is a 2.0 decided in an ADR."
exit 1
fi
90 changes: 90 additions & 0 deletions .github/workflows/publish-app-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Publish app contract

# Publishes crates/app-contract (octosense-app-contract, OctoSense ADR 0005)
# to crates.io. A published version can never be deleted, only yanked, so
# this runs only by hand or from a release tag, after the version bump and
# CHANGELOG entry were reviewed by App Hub and an app owner (Rinx).
#
# by hand Actions → Publish app contract → Run workflow (on main)
# by tag git tag app-contract-v1.0.0 && git push origin app-contract-v1.0.0
# (the tag must name the crate's version)
#
# Needs the repository secret CARGO_REGISTRY_TOKEN: a crates.io token scoped
# to publish-update for octosense-app-contract (publish-new for the first
# release). The job runs in the crates-io environment, where reviewers can
# be required before the token is handed out.

on:
workflow_dispatch:
push:
tags:
- 'app-contract-v*'

permissions:
contents: read

concurrency:
group: publish-app-contract
cancel-in-progress: false

env:
CARGO_TERM_COLOR: always
CRATE: octosense-app-contract

jobs:
publish-app-contract:
if: github.repository == 'OctoSense-org/OctoSense-App-Hub'
runs-on: ubuntu-latest
environment: crates-io
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Copy the crate out of the workspace
# The workspace's [patch] tables name sibling checkouts; the contract
# builds from crates.io alone, as its users will build it.
shell: bash
run: cp -R crates/app-contract "$RUNNER_TEMP/app-contract"
- name: Refuse a version that is already published
id: version
working-directory: ${{ runner.temp }}/app-contract
shell: bash
run: |
set -euo pipefail
version=$(cargo metadata --no-deps --format-version 1 | jq -r --arg crate "$CRATE" '.packages[] | select(.name == $crate) | .version')
echo "version=$version" >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" != "app-contract-v$version" ]]; then
echo "::error::Tag $GITHUB_REF_NAME does not name the crate's version $version (expected app-contract-v$version)."
exit 1
fi
status=$(curl --proto '=https' --tlsv1.2 --silent --show-error --location \
--output "$RUNNER_TEMP/index" --write-out '%{http_code}' \
"https://index.crates.io/oc/to/$CRATE")
case "$status" in
200)
if jq -e --arg v "$version" 'select(.vers == $v)' "$RUNNER_TEMP/index" > /dev/null; then
echo "::error::$CRATE $version is already on crates.io. Bump the version (and CHANGELOG.md) to publish again."
exit 1
fi
;;
404) echo "$CRATE is not on crates.io yet: this is its first release." ;;
*)
echo "::error::crates.io index answered HTTP $status; cannot tell whether $version is published."
exit 1
;;
esac
echo "Publishing $CRATE $version."
- name: Tests and the fixture corpus
working-directory: ${{ runner.temp }}/app-contract
run: cargo test --all-targets && cargo test --doc
- name: Publish
working-directory: ${{ runner.temp }}/app-contract
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
shell: bash
run: |
set -euo pipefail
: "${CARGO_REGISTRY_TOKEN:?Configure the CARGO_REGISTRY_TOKEN secret}"
# The copy is outside git, so there is nothing for Cargo to find
# dirty; --allow-dirty only silences that check.
cargo publish -p "$CRATE" --allow-dirty
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# The OctoSense app hub: the crates behind the catalog in this repository.
#
# app-contract the app contract (OctoSense ADR 0005), published to
# crates.io: manifest, app policy, integrity, running a
# package; depends on crates.io crates only
# app-policy the signed manifest and listing; admission; resolution into
# an isolate's settings and an agent session profile (ADR 0002)
# app-hub the index, the signed catalog, the gate, the scan, the device
Expand All @@ -17,6 +20,7 @@
[workspace]
resolver = "2"
members = [
"crates/app-contract",
"crates/app-policy",
"crates/app-hub",
"crates/appstore",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ publisher's repository.
| `docs/ICONS.md` | Canonical icon ownership, export constraints and visual review. |
| `docs/DEVELOPMENT.md` | Where authoring lives, delivery paths, and `card-host` with its remote-control routes. |
| `templates/app/` | Card app repository scaffold with metadata, example icon and linked agent instructions. |
| `crates/app-policy` | The signed manifest and listing, admission, and resolution into an isolate's settings and an agent session profile (ADR 0002); an app's own agent (`tools.json`, `AGENT.md`, skills) loaded as an `AgentBundle`; the `tools.json` parser and checks that native modules' tool manifests share (`ToolManifest::load`). |
| `crates/app-contract` | The app contract, `octosense-app-contract` on crates.io (OctoSense ADR 0005): the manifest, the policy an app gets, package integrity and running a package. Apps and hosts depend on it by version; within `1.x` it only grows ([README](crates/app-contract/README.md)). |
| `crates/app-policy` | The signed manifest and listing, admission, and resolution into an isolate's settings and an agent session profile (ADR 0002); an app's own agent (`tools.json`, `AGENT.md`, skills) loaded as an `AgentBundle`; the `tools.json` parser and checks that native modules' tool manifests share (`ToolManifest::load`). Re-exports the app contract. |
| `crates/app-hub` | The index, the signed catalog, the gate, the agent scan, the device client and the `hub` command (ADR 0003). |
| `crates/appstore` | The store as an OctoSense module, the `card` module that runs an installed app as its own client, system apps (`os.` ids) and host services with their sheets. |
| `crates/appstore-app` | The store as a standalone app (`appstore`). |
Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
| `docs/ICONS.md` | 规范图标的归属、导出约束与视觉审查。 |
| `docs/DEVELOPMENT.md` | 应用编写指南所在的仓库、交付路径,以及 `card-host` 及其远程控制路由。 |
| `templates/app/` | 卡片应用仓库脚手架,包含元数据、示例图标和链接好的 Agent 指引。 |
| `crates/app-policy` | 签名清单与商店信息、准入,以及解析为隔离环境设置和 Agent 会话配置(ADR 0002);应用自带的 Agent(`tools.json`、`AGENT.md`、技能)加载为 `AgentBundle`;原生模块的工具清单共用同一套 `tools.json` 解析与检查(`ToolManifest::load`)。 |
| `crates/app-contract` | 应用契约,即 crates.io 上的 `octosense-app-contract`(OctoSense ADR 0005):清单、应用获得的策略、包完整性校验与运行包所需的内容。应用和宿主按版本依赖它;在 `1.x` 内只做增量变更([README](crates/app-contract/README.md))。 |
| `crates/app-policy` | 签名清单与商店信息、准入,以及解析为隔离环境设置和 Agent 会话配置(ADR 0002);应用自带的 Agent(`tools.json`、`AGENT.md`、技能)加载为 `AgentBundle`;原生模块的工具清单共用同一套 `tools.json` 解析与检查(`ToolManifest::load`)。并重新导出应用契约。 |
| `crates/app-hub` | 索引、签名目录、准入检查、Agent 扫描、设备端客户端和 `hub` 命令(ADR 0003)。 |
| `crates/appstore` | 作为 OctoSense 模块的商店;把已安装应用作为独立客户端运行的 `card` 模块;系统应用(`os.` 前缀 id);以及宿主服务及其面板。 |
| `crates/appstore-app` | 作为独立应用的商店(`appstore`)。 |
Expand Down
32 changes: 32 additions & 0 deletions crates/app-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changelog

`octosense-app-contract` follows the rules in [README.md](README.md#stability):
within `1.x` it only grows.

## 1.0.0

The contract as OctoSense ADR 0005 section 1 defines it, moved out of App
Hub's `octosense-app-policy` with its behaviour unchanged:

- Manifest: `AppManifest` and its parts, `SCHEMA`, `MANIFEST_FILE`, `parse`.
- Policy: `policy::resolve`, `HostLimits`, `AppPolicy` (what the app may
do: capabilities, hosts, storage, budgets, research scope). The app's
agent is resolved by hosts that run agents, not here.
- Integrity: `digest_dir`, `bundle_digest`, `admit`, `admit_digest`,
`SignatureVerifier`, `RefuseAllSignatures`.
- Running a package: `SCRIPT_ENTRY`, `script_source`, `ASSETS_PLACEHOLDER`,
`AssetServer`, `StaticAssets`, `rewrite_assets`.

New in the contract:

- `requires` and `schema_minor` in the manifest, `KNOWN_FEATURES` (empty)
and `SCHEMA_MINOR` (0): a manifest requiring an unknown feature is refused
("needs a newer host"); a manifest for a newer `1.x` is read with its
unknown (optional) fields ignored and listed by
`AppManifest::ignored_fields`.
- Every public struct and enum is `#[non_exhaustive]` (except the unit
marker `RefuseAllSignatures`); `HostLimits` gains `with_*` builders and
`Signature` gains `new`.
- `AppPolicy` carries the whole storage block (`StorageGrant`) and
serialises.
- The fixture corpus (`tests/fixtures/`).
24 changes: 24 additions & 0 deletions crates/app-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# octosense-app-contract — OctoSense ADR 0005: the one small, versioned
# interface between App Hub and every app. The manifest, the policy an app
# gets, the integrity checks and what running a package needs; nothing of
# App Hub's store, agents or sandboxes.
#
# Published to crates.io, so it may depend on crates.io crates only: no git
# and no path dependencies. Within 1.x it only grows (see README.md).
[package]
name = "octosense-app-contract"
version = "1.0.0"
edition = "2021"
license = "Apache-2.0"
description = "The OctoSense app contract: app manifests, the policy an app gets, package integrity and running a package"
repository = "https://github.com/OctoSense-org/OctoSense-App-Hub"
homepage = "https://github.com/OctoSense-org/OctoSense-App-Hub/tree/main/crates/app-contract"
documentation = "https://docs.rs/octosense-app-contract"
readme = "README.md"
keywords = ["octosense", "manifest", "sandbox", "app", "policy"]
categories = ["config", "parser-implementations"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
blake3 = "1"
Loading
Loading