Repository navigation
octosense-app-contract 1.0.0: the app contract (OctoSense ADR 0005) - #46
Merged
Merged
Conversation
…licy
OctoSense ADR 0005 step 2. The new crate crates/app-contract holds exactly
the contract surface of ADR section 1, moved out of octosense-app-policy
with its behaviour unchanged:
- manifest: AppManifest and its parts, SCHEMA, MANIFEST_FILE, parse
- policy: policy::resolve, HostLimits, AppPolicy (capabilities, hosts,
storage, budgets, research scope)
- integrity: digest_dir, bundle_digest, admit, admit_digest,
SignatureVerifier, RefuseAllSignatures
- running a package: SCRIPT_ENTRY, script_source, ASSETS_PLACEHOLDER,
AssetServer, StaticAssets, rewrite_assets
It depends on serde, serde_json and blake3 from crates.io only.
AppPolicy is split: the contract's AppPolicy is what the app may do;
app-policy's AppPolicy is the host type { app, agent } and derefs to the
contract's, so field reads and allows()/allows_host() read as before. The
agent is resolved after the contract's rules, in the same order as before.
IsolateSettings, the Splash adapter, agents, listings, research words and
the service registry stay in app-policy, which depends on the contract by
path and version and re-exports it (as `contract`, and item by item).
Growth rules (ADR section 2): the parser stays strict; the manifest gains
`requires` and `schema_minor` (default 0), both skipped from the signing
bytes when empty; a manifest requiring a feature outside KNOWN_FEATURES
(empty in 1.0.0) is refused with "needs a newer host: <feature>". The
manifest's structs and enums, ResearchScope and AppPolicy are
non_exhaustive so they can grow within 1.x. Documented in the crate docs'
Stability section and the crate README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 fixtures under crates/app-contract/tests/fixtures: the shell's system apps (OctoSense apps/*: ai-providers, camera, mail, maps, news, youtube), App Hub's app template and news-agent test bundle, two Rinx mini apps (one with its shipped digest), the admission tests' cases (net and hosts, research and crawl scopes, the storage block, host services, schema_minor) and ten refusals (an unknown required feature, an unknown field, a foreign schema, a digest mismatch, unsigned, a reserved id...). Each pins the package digest, the manifest's signing-bytes digest, the entry kind and the resolved AppPolicy (or the refusal). tests/fixtures.rs loads every one and compares; a later 1.x may add policy fields or say more in a refusal, never something else. APP_CONTRACT_BLESS=1 fills in a new fixture's missing keys and never changes existing ones: the corpus is append-only within 1.x (fixtures/README.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/workflows/app-contract.yml, on changes to crates/app-contract: the crate is copied out of the workspace (whose [patch] tables name sibling checkouts it does not use) and built as crates.io will build it: tests with the fixture corpus, doc tests, clippy -D warnings, cargo package --list and cargo publish --dry-run. The semver job runs cargo semver-checks against the latest published version from crates.io's sparse index, so a non-additive change fails the pull request (ADR 0005 sections 2 and 5). Until a first version is published the index answers 404 and the job skips with a notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/workflows/publish-app-contract.yml runs on workflow_dispatch or an app-contract-v<version> tag (which must name the crate's version). It refuses a version crates.io already has, runs the tests and the fixture corpus, and publishes octosense-app-contract with the CARGO_REGISTRY_TOKEN secret, in the crates-io environment so reviewers can be required before the token is used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README tables list crates/app-contract, and PUBLISHING.md points at the contract's entry, manifest and research files, which moved there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 1.x
Two things 1.0.0 must get right, since a published version is permanent.
Every public struct and enum is #[non_exhaustive] (the unit marker
RefuseAllSignatures excepted, so `&RefuseAllSignatures` still builds), so
a field or variant added in 1.x is a minor change. HostLimits gains
with_* builders and Signature gains new(); App Hub's call sites use them
(`HostLimits { require_signature: x, ..HostLimits::default() }` becomes
`HostLimits::default().with_require_signature(x)`).
ADR 0005 section 2's optional/required rule, in full:
- schema must be 1, and every `requires` entry must be in KNOWN_FEATURES,
at every schema_minor ("needs a newer host: <feature>");
- schema_minor <= SCHEMA_MINOR: read strictly, as before;
- schema_minor > SCHEMA_MINOR: unknown fields at any level are optional by
rule, so they are ignored (a deserializer over serde_json::Value drops
the keys each struct does not name) and listed, sorted, by
AppManifest::ignored_fields(). Known fields are checked as always, and
the old research shape is still refused. The ignored fields are put back
into the signing bytes, so a newer signed manifest still verifies.
AppPolicy also carries the manifest's whole storage block as StorageGrant
(max_bytes clamped, accounts, agent_workspace, cache_max_bytes); the
fixtures' pinned policies gain it and the ignored fields, nothing else.
`external` is not part of a script app's manifest, so it is not there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test-newer-minor-optional (schema_minor 2, unknown optional fields at the top level, in network and in storage: admitted, fields reported) and refuse-newer-minor-requires (an unknown required feature: refused). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
octosense-app-contract 1.0.0 (OctoSense ADR 0005, step 2)
Creates
crates/app-contract, the crate apps and hosts will depend on by version (octosense-app-contract = "1"). Its code is moved out ofoctosense-app-policywith the behaviour unchanged, and app-policy re-exports it. This PR does not publish anything. Publishing is the manualpublish-app-contractworkflow, run after review.The contract surface (ADR §1)
AppManifestand its parts (Integrity,Signature,Network,Storage,Compute,AgentSpec,ModelSpec,TaskModel,Triggers,ProfileMode,ModelNeed,ModelTier,AgentWorkspace,ResearchScope),SCHEMA,SCHEMA_MINOR,KNOWN_FEATURES,KNOWN_CAPABILITIES,MANIFEST_FILE,parsepolicy::resolve,HostLimits(withwith_*builders),AppPolicy.AppPolicyholds the capabilities, hosts, the instruction and memory budgets,may_prompt, the research scope, and the whole storage block asStorageGrant(max_bytesclamped,accounts,agent_workspace,cache_max_bytes).externalisn't in a script app's manifest, so it isn't inStorageGranteither.digest_dir,bundle_digest,admit,admit_digest,SignatureVerifier,RefuseAllSignaturesSCRIPT_ENTRY,script_source,ASSETS_PLACEHOLDER,AssetServer,StaticAssets,rewrite_assetsThe contract depends on three crates.io crates and nothing else:
serde(withderive),serde_jsonandblake3.These stay in app-policy:
IsolateSettingsandSessionProfile,splash_adapter, agents (AgentPolicy,AgentBundle,KERNEL_TOOLS), listings, the research words and the service registry.The AppPolicy split
AppPolicycovers what the app may do. It is#[non_exhaustive]and serialises.AppPolicyis now{ app: contract::AppPolicy, agent: Option<AgentPolicy> }and derefs to the contract's type. Field reads andallows/allows_hostwork as before.policy::resolveruns the contract's rules first, then resolves the agent. That is the same order and the same error messages as before.policy.research.unwrap(), now has to writepolicy.app.research. Two of App Hub's own tests needed this change. OctoSense and Rinx v1.0.2 compile unchanged.Growth rules (ADR §2)
Additive growth without 2.0. Every public struct and enum is
#[non_exhaustive].RefuseAllSignatures, because&RefuseAllSignaturesmust stay constructible.String.HostLimits::default()orHostLimits::system(), pluswith_*buildersSignature::newparsefor manifests,policy::resolvefor policies#[non_exhaustive]type, adding a public field or an enum variant is a minor change forcargo semver-checks. Adding the attribute later would be a major change, which is why it goes in now.Struct literals that must change:
hub.rs,gate.rs,card-host/src/host.rs,signing.rs(nowSignature::new) and three test files. All are updated in this PR.&HostLimits { require_signature: false, ..Default::default() }becomes&HostLimits::default().with_require_signature(false)at:tools/miniapp-package/src/main.rs:19crates/system-apps/src/lib.rs:170src/miniapps/package.rs:103Unknown fields are classified, not ignored.
parseapplies these rules in order:schemamust be 1.requiresentry must be inKNOWN_FEATURES, at everyschema_minor. Otherwise the manifest is refused withapp <id> needs a newer host: <feature>.schema_minor≤SCHEMA_MINOR, the manifest is parsed strictly, exactly as before.schema_minor>SCHEMA_MINOR, unknown fields at any level are ignored:serde_json::Value, using the field names each struct passes to serde.AppManifest::ignored_fields()lists them, sorted.In 1.0.0,
KNOWN_FEATURESis empty andSCHEMA_MINORis 0. Both new manifest fields are left out of the signing bytes when they are empty.The rules are documented in the rustdoc "Stability" section and in
crates/app-contract/README.md.Fixtures
tests/fixtures/holds 27 fixtures:Each fixture pins:
AppPolicy, or the refusal messageThe corpus is append-only within 1.x.
APP_CONTRACT_BLESS=1only fills in keys that are missing.CI
There were no workflows on main, so both workflows are new. Both copy the crate out of the workspace, because the workspace's
[patch]tables need the sibling checkouts.app-contract.ymlhas two jobs:-D warnings,cargo package --listandcargo publish --dry-runcargo semver-checksagainst the latest version on crates.io. It skips with a notice while the sparse index returns 404.publish-app-contract.ymlruns fromworkflow_dispatchor anapp-contract-v*tag:secrets.CARGO_REGISTRY_TOKENin thecrates-ioenvironmentChecks
cargo test --workspace-D warningson the contract and app-policyoctosense-app-policy --features splashcargo package --listcargo publish --dry-run[patch]pointing at this branch:cargo check -p octosensephonewithcargo check --features mobile-appsThe local edits, none of them committed:
[patch]ed locally. Unmodified, Rinx v1.0.2 fails at exactly thoseHostLimitsliterals (E0639).ServiceCall.may_prompt, which App Hub Host requests always settle, and the surface decides about sheets #38 added and OctoSense hasn't adopted yet. This PR doesn't cause it.Before publishing
CARGO_REGISTRY_TOKENsecret (publish-new scope for the first release).crates-ioenvironment.🤖 Generated with Claude Code