From 4e3139b218aaca1045af2d12b8debb710270c946 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 18 Oct 2026 21:44:09 +0300 Subject: [PATCH 1/5] docs: clarify Windows traversal normalization --- MODULE.md | 7 +++++-- crates/tinysecurity-bus/src/path_batch.rs | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/MODULE.md b/MODULE.md index 32c86bcf..e87a3f43 100644 --- a/MODULE.md +++ b/MODULE.md @@ -49,8 +49,11 @@ Path checks require the host's absolute operation-resolved target, resolve exist ancestors including symlinks, and enforce the credential/system/traversal floor on lexical and resolved forms even when policy is disabled. Traversal is any segment that starts with `..` and holds only dots and -spaces (`..`, `.. `, `...`), on every OS, since Windows normalizes those to `..`; such a -`forbidden_paths` entry is rejected at registration. Unknown native path dialects, broken symlinks and failed +spaces (`..`, `.. `, `...`), on every OS. Windows treats exact `..` as a parent +component; some Windows path APIs separately normalize trailing spaces or periods. +The policy also rejects `...` as a conservative cross-platform rule, even though +it can remain a distinct name on Windows. Such a `forbidden_paths` entry is rejected +at registration. Unknown native path dialects, broken symlinks and failed resolution deny. Enabled action roots must resolve to existing directories; workspace and configured forbidden roots deny before approval. New files can be checked through their existing parent. Permission does not guarantee diff --git a/crates/tinysecurity-bus/src/path_batch.rs b/crates/tinysecurity-bus/src/path_batch.rs index a7786ccd..977c485a 100644 --- a/crates/tinysecurity-bus/src/path_batch.rs +++ b/crates/tinysecurity-bus/src/path_batch.rs @@ -61,9 +61,12 @@ pub struct PathBatchResponse2 { /// Query the unconditional protected-path floor for paths outside any scope. /// /// The floor is lexical and needs no registered policy: credential stores, core -/// system roots, parent-directory traversal (`..` and the forms Windows normalizes to it, -/// such as `.. ` and `...`) and NUL bytes. It holds whether or not the host's -/// discretionary policy is enabled. +/// system roots, parent-directory traversal (`..` and conservative cross-platform +/// forms such as `.. ` and `...`) and NUL bytes. Windows treats exact `..` as a +/// parent component; some Windows path APIs separately normalize trailing spaces +/// or periods, while `...` can remain a distinct name. The policy rejects these +/// forms on every OS. The floor holds whether or not the host's discretionary +/// policy is enabled. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ProtectedPathRequest2 { From 581f72eedb3bedf7e5622f0c98d2712198099ef4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 18 Oct 2026 21:47:37 +0300 Subject: [PATCH 2/5] docs: align traversal semantics across contract --- MODULE.md | 6 ++++-- crates/tinysecurity-policy/src/path.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/MODULE.md b/MODULE.md index e87a3f43..a55820eb 100644 --- a/MODULE.md +++ b/MODULE.md @@ -509,8 +509,10 @@ cannot bind the call answers an error, never an allow. Latency budgets are per c - `ProtectedPath2` (`ProtectedPathRequest2` -> `ProtectedPathResponse2`): the unconditional lexical floor with no scope and no filesystem access: credential stores, system and installation roots, parent-directory traversal (`..` and any segment of two or more dots - followed only by dots and spaces, such as `.. ` or `...`, which Windows normalizes to `..`), NUL bytes and Windows roots and extended prefixes, on every - OS. At most 4096 paths. Budget: in memory, sub-millisecond. + followed only by dots and spaces, such as `.. ` or `...`), NUL bytes and Windows roots and + extended prefixes, on every OS. Exact `..` is a Windows parent component; trailing spaces + and periods are a separate API-specific normalization case, while `...` can remain a + distinct Windows name. At most 4096 paths. Budget: in memory, sub-millisecond. - `ProtectedFloor2` (no arguments): the floor's vocabulary (credential directories, single-file stores, segment pairs, lower-cased absolute prefixes), built from the constants the matcher uses, so a host handing it to a jail grant resolver cannot drift from `ProtectedPath2`. diff --git a/crates/tinysecurity-policy/src/path.rs b/crates/tinysecurity-policy/src/path.rs index f2094f9a..e5eb9714 100644 --- a/crates/tinysecurity-policy/src/path.rs +++ b/crates/tinysecurity-policy/src/path.rs @@ -46,8 +46,10 @@ pub(super) const WINDOWS_SYSTEM_ROOTS: &[&str] = &[ "program files (x86)", "programdata", ]; -/// A parent-directory segment, including the forms Windows normalizes to `..` -/// (trailing spaces and dots, as in `.. ` or `...`). Checked lexically on every OS. +/// A parent-directory segment, including conservative cross-platform forms with +/// trailing spaces or dots (such as `.. ` or `...`). Windows treats exact `..` +/// as a parent component; some Windows path APIs separately normalize trailing +/// spaces or periods, while `...` can remain a distinct name. Checked on every OS. pub(super) fn parent_segment(segment: &str) -> bool { segment.starts_with("..") && segment.trim_end_matches([' ', '.']).is_empty() } From 14bc15c6c03b2959a359f01a6f3a929f26a8db27 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 18 Oct 2026 22:09:46 +0300 Subject: [PATCH 3/5] docs: split tinysecurity module guide --- MODULE.md | 223 +------------------------------- README.md | 3 +- docs/README.md | 2 + docs/module-crypto.md | 173 +++++++++++++++++++++++++ docs/module-subject-policies.md | 51 ++++++++ 5 files changed, 233 insertions(+), 219 deletions(-) create mode 100644 docs/module-crypto.md create mode 100644 docs/module-subject-policies.md diff --git a/MODULE.md b/MODULE.md index a55820eb..3a4bb14c 100644 --- a/MODULE.md +++ b/MODULE.md @@ -93,8 +93,8 @@ Sandbox planning ([sandbox](crates/tinysecurity-sandbox/README.md)) selects and a plan; TinyBox enforces isolation at launch. The optional judge ([scoring](crates/tinysecurity-auto/README.md)) only scores requests that still need approval and never grants human-only authority. Keyring consent and the password and -pairing members are in the confidential crypto section below. Unsupported member calls -return the bus's unknown-member error. +pairing members are documented with the other [confidential crypto members](docs/module-crypto.md). +Unsupported member calls return the bus's unknown-member error. ## Packaging and smoke checks @@ -119,226 +119,13 @@ release budget in [performance](docs/performance.md). OpenHuman's host-side view the [security architecture page](https://github.com/tinyhumansai/openhuman/blob/main/gitbooks/developing/architecture/security.md). ## Confidential device handles - -Optional `ModuleConfig.device: DeviceConfig2` adds `EvaluateDevice2` and -`ExecuteDevice2` to the native interface. Existing omitted-device payloads and -v2/v3/v4 wire frames keep their serialized forms. These members require TinyBus -confidential transport and the immutable verified `trusted_host` peer. Missing -configuration, another sender, public identity claims, unconfidential calls, -missing callbacks and required audit failures deny native authority. - -Each configured device scope references a subject already registered in -`approvals`, captures its verified origin, and optionally consumes one static -X25519 credential during SDK init. Absent credentials generate native static -keys. Neither variant exports private material. Device calls select only an -existing scope and a bounded operation: issue a static/ephemeral key handle, -derive a role-bound tunnel, seal/open v2, seal/open v3 handshake or v4 ack, -revoke a handle, or cancel the authenticated scope. Public keys retain the -canonical 32-byte URL-safe unpadded base64 format. - -Evaluate builds the exact content digest internally and uses the existing -verified approval callbacks. Execute requires the resulting exact human Once -receipt and fresh required audit correlation; automatic, durable-tool or -caller-asserted decisions cannot issue native authority. Every callback/worker -await is followed by live generation/scope/deadline checks. Plaintext, keys, -frames and handles do not reach approval/audit payloads, errors, Debug or -`PolicyInfo`. - -Explicit init bounds cover registrations, handles, concurrent reservations, -per-scope rate, TTL, message bytes and retained bootstrap nonces. Reinit may -change bounds without reimporting credentials or changing immutable subjects. -It invalidates all old handles; successful new-generation receipts issue new -handles. Revoke removes dependent tunnels; scoped Cancel fences pending work. -Bootstrap replay evidence survives alias issuance, Cancel and reinit and fails -closed at capacity. Clock rollback/unavailability faults device authority for -that process. Shutdown closes owned credentials even when an SDK observer -retains the connection. See the [owned cleanup and replay limits](crates/tinysecurity-crypto/README.md#scoped-device-handles). - -`device_native_contract` loads the hashed shared library and exercises actual -confidential dispatch, verified approval/audit callback continuity, v2/v3/v4, -replay/revoke, valid generation-two receipts, old-handle denial and SDK shutdown. -Run the retained runner with the absolute library path from both owner and -non-owner working directories. Linux proves Linux behavior; macOS and Windows -require their native runners. This does not release artifacts or migrate a host. - -## Confidential injected master resolution - -Contract 1.10 implements optional `ModuleConfig.secret` and confidential -`EvaluateSecret2` / `ExecuteSecret2`. The only operations are -`ResolveConfigMaster` and `ResolveAppMaster`. Init registers metadata: an exact -existing approval scope, purpose, opaque backend descriptor, target named slot -and at most three compatible historical readers. App authority is separately -registered; calls cannot override subjects, origins, backends, slots or paths. - -Evaluation retains the same bounded native reservation, private challenge and -keyed safe correlation through execution. Registered auto approval, reusable -choices and judges cannot issue this grant. Actual confidential `VerifyHuman`, -Human Once decision and reservation CAS, and the exact required audit sink -receipt finish before backend load or mint. A canceled begin holds its admitted -reservation until expiry; retry requires a fresh original call id. There is no -second wait queue. - -The fixed `SecretBackend2` callback has confidential `Load` and `Set` only. -Every reply echoes backend, registration, generation, operation id, phase and -slot before status interpretation. Denial is not absence. Encodings are bounded -to 4096 bytes. A valid target skips all historical readers; compatible sources -must parse and agree before mutation. Their original uppercase/whitespace -encoding is copied unchanged and verified by exact target readback. No source -is deleted. File is only a named injected store entry, not physical file access. - -One resolver and exclusive callback context persist for each immutable binding. -Live native authority is checked before and after each await and before mint -and publication. Cancellation after Set begins, timeout, wrong readback and -indeterminate mutation permanently fence that resolver. The host callback broker -must independently serialize each actual target slot, retain detached provider -workers and block fresh reads/mint/Set until trusted quiescence and exact readback. -Operation ids and Stored acknowledgements do not provide generic CAS or durability. -Recreating a resolver, restarting a process or dropping a service tree is not -recovery authority. This checkpoint provides no recovery member. - -Results contain a bounded random alias, purpose and generation only. The master -and underlying native handle remain internal, with no alias-use/read/export -member. Aliases are pruned on expiry or revocation and cleared at close. Owned -successfully decoded callback values and unpublished output are wiped; SDK -serialization, frame copies and partially decoded serde values are outside that -cleanup guarantee. - -Configured-secret SDK reinit keeps the same process owner, store brokers and -master resolvers. Native and device transitions retain their state locks through -policy publication; valid-clock publication rejection preserves old authority. -Accepted generations invalidate pending native work and clear aliases, while -persistent resolver uncertainty remains unavailable. Native clock faults still -close authority permanently. Device replay capacity and every scope epoch are -checked before publication; bootstrap evidence survives accepted reinit. Physical files, plaintext delivery and routine native consumers remain outside -this subset; password, OS keyring and pairing members are in contract 1.14 below -(`PasswordCrypt2`, `KeyringAccess2`, `PairingControl2`). - -## Confidential native config cipher (contract 1.11) - -`EvaluateSecretCipher2` and `ExecuteSecretCipher2` are confidential routine -operations over an actual `ExecuteSecret2` ConfigUserMaster acquisition. They -never prompt for routine fields or create master authority from a receipt. -`secret_cipher` SDK metadata fixes one existing registration, consumer identity, -enabled setting, callback timeout and physical slot count. It cannot change on -reinit. Older 1.10 initialization omits it and retains its original behavior. -An older decoder does not know the additive `native_routine` audit kind; hosts -using this capability must negotiate the new contract vocabulary. - -Evaluate owns the real private crypto pending reservation, then commits required -sanitized NativeRoutine intent through the existing AuditClient. Execute removes -it once, requires unchanged metadata and confidential input, and checks actual -execution, output-stage and publication audit acknowledgements. Audit arguments -contain keyed operation/output correlations, Engine-derived durable scope/policy -and fixed classifications. Caller-supplied audit event identities reserve stream -order; they never construct a private routine acknowledgement or cipher grant. - -Plaintext, disabled/empty passthrough and disabled migration replacements go only -to the fixed confidential `SecretCipherConsumer2.Deliver` port. A real trusted -receiver authenticates the pinned loaded module and stages zeroizing output in -an exclusive bounded physical slot. It refuses overwrite and releases locally -only after the matching successful owner response. A staged callback reply is -not product plaintext publication. The owner retains its original reservation -through final required audit and live validation; only true ciphertext appears -in ordinary result fields. A failed/canceled/late/invalid stage leaves its sender -slot permanently occupied across generation changes and reinit. Operation-ID -or status echoes cannot clear uncertainty. A successful response permits the -receiver to consume/wipe its exact slot before accepting another stage there. - -The broker currently requires every confidential destination to be an admitted -native module. Ordinary unattested host callbacks are denied, including master -HumanOnly prompt and secret backend delivery. The tracked native fixture loads -the real hashed TinySecurity cdylib and a separately admitted host-trusted native -callback fixture. This proves module-to-admitted-native-consumer composition; -it does not prove ordinary OpenHuman host callback admission. That TinyBus -integration gap remains open. SDK serde/frame/partial-decode copies and process -abort are outside the owned-buffer wiping guarantee. App document storage remains a separate unfinished capability. +Full details: [`Confidential device handles`](docs/module-crypto.md#confidential-device-handles). ## Registered subject policies (contract 1.12, extended in 1.13) - -`RegisterPolicy(RegisteredPolicy)` returns an opaque `PolicyId` binding one -verified subject to a registered path scope, a registered command scope, -nonsensitive `PolicySettings` (base policy, privacy, egress hosts, permitted -command classes, auto-approve origins, judge opt-in) and ordered `ToolRule`s. -Registering the same value again returns the same id; a different value for an -active subject is rejected and must go through `SupersedePolicy`. Ids are private -references, not credentials, and registration is a trusted-host operation. - -`EvaluateScoped` and `CheckScoped` take a `PolicyId`, the verified -`CallerContext` and the original call or checks. The caller's agent must equal -the registered subject, so one tenant cannot use another's id. Decisions carry -the module generation, the policy's lineage generation and a SHA-256 call -fingerprint over the policy id, caller (excluding only the call instance) and -the original call. The fingerprint correlates and binds a decision to its call; -it does not make the decision reusable by itself. Hosts may reuse a decision only -when its `cacheable` flag is true. Scoped decisions are cacheable for -`LocalDiagnostic` and `Path` checks and for tool-rule denials, and never for command -checks (they reserve budget), foreign or retired callers, or audited members; key a -cache on the whole fingerprint plus both generations. The registered filesystem scope -applies on top of the base settings, and commands run through the registered command -scope, so budgets and classes are per subject. A superseded id denies with `invalid_context`. Unbound scoped -members return `approval_unavailable` for permitted work while required audit is -configured; `EvaluateScopedAudited` and `CheckScopedAudited` (1.13) take the same -`AuditedRequest2` wrapper as `EvaluateAudited2`/`CheckAudited2` and are the permitted -path under required audit. `PolicySettings.policy.forbidden_paths` accepts `~`, -relative (resolved against `action_dir`) and floor entries; see -[`docs/specs/scoped-policies.md`](docs/specs/scoped-policies.md). - -`SupersedePolicy` replaces a subject's active policy atomically; the old policy -stays active on any failure. If the replacement widens authority (new or broader -roots, removed restrictions, new hosts or tools, higher command autonomy, -classes, allowlist entries or budget, a command scope or execution -context that grants more (a different scope id alone is not a widening), new auto origins, judge enabled, -enforcement or the registered path scope disabled, or privacy loosened) the response is `ReviewRequired` with the exact -`policy_change` call. The host passes it to `EvaluateApproval2` under the old -policy's approval scope; after a human decision it repeats the request with -`PolicyChangeApproval`, which reserves the approval once before activation. Only a -row decided by an authenticated human qualifies; automatic, judge and reused -approvals are refused. A -narrowing change activates without review. Committed activation emits `PolicyChanged` -carrying the new `policy_id` and `policy_generation`. - -`PolicyInfo` adds nonsensitive judge settings, callback and authority -availability, and the active scoped-policy count; `ScopedPolicyInfo(PolicyId)` -returns one policy's effective settings and rules. No credential, endpoint or -scope handle appears in either. `max_cost_per_day_cents` is not part of the -contract: see ADR 0006. +Full details: [`Registered subject policies (contract 1.12, extended in 1.13)`](docs/module-subject-policies.md#registered-subject-policies-contract-112-extended-in-113). ## Confidential password, keyring and pairing members (contract 1.14) - -Optional `ModuleConfig.crypto: CryptoConfig2` adds three confidential members, -`PasswordCrypt2`, `KeyringAccess2` and `PairingControl2`. Like the device and -secret members they require TinyBus confidential transport and the immutable -verified `trusted_host` peer; an unconfigured module denies all three. Config is -init-only and immutable across reinit. It bounds payload bytes (at most 1 MiB) -and operations per monotonic minute (at most 4096). - -`PasswordCrypt2` preserves the existing Argon2id v0x13 (64 MiB, 3 passes, 1 lane) -plus AES-256-GCM format. The payload keeps the legacy byte-array shape -(`ciphertext`, `nonce`, `salt`); encrypt generates a sixteen-byte salt unless the -caller supplies one for the legacy separate-salt layout. Frozen host fixtures -decrypt unchanged. At most two derivations run at once and there is no wait queue. - -`KeyringAccess2` reads, writes or deletes a named secret on a backend chosen at -init: platform default (Keychain, Credential Manager, Linux kernel keyring), -Linux kernel keyring, or a protected encrypted file in an absolute directory. -Linux Secret Service is not compiled into this module and fails closed. Every -operation first calls the host's `ai.tinyhumans.tinysecurity.KeyringConsent2` -`Consent` callback with the verified caller, purpose, access kind and secret name -(never the value), and proceeds only on an explicit allow. Invalid names or -purposes, oversized values and denied or timed-out consent never reach the backend. - -`PairingControl2` exposes `Status`, `Authenticate` (constant-time bearer check -against legacy tokens), `Invalidate` and `Revoke` for accounts registered at init. -Challenge issuance and numeric verification (`Begin`/`Verify`) are not exposed: -they need confidential delivery to an admitted native destination, the same open -TinyBus gap as the config cipher consumer. Their ports fail closed. - -Passwords, plaintext, secret values and bearers are wiped after use and never -appear in `Debug`, errors, `PolicyInfo` (only `callbacks.crypto`) or audit rows. -The X25519/HKDF device tunnel remains the `EvaluateDevice2`/`ExecuteDevice2` -surface above. The OS keyring test runs on every platform and is required to -use the real credential store when `TINYSECURITY_REQUIRE_OS_KEYRING=1`; it -always removes its entry. +Full details: [`Confidential password, keyring and pairing members (contract 1.14)`](docs/module-crypto.md#confidential-password-keyring-and-pairing-members-contract-114). ## Credentials-only redaction and open public egress (contract 1.15) diff --git a/README.md b/README.md index a7b8dc16..0191b33b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ Use `.dylib` on macOS and `tinysecurity_module.dll` on Windows. The loader example proves allow/deny, malformed/unsupported invocation rejection, reconfiguration retention and generation, then prints real bus latency. -See [module boundary](MODULE.md), [accepted specification](docs/specs/security-module.md), +See [module boundary](MODULE.md), [confidential crypto members](docs/module-crypto.md), +[registered subject policies](docs/module-subject-policies.md), [accepted specification](docs/specs/security-module.md), [implementation sequence](docs/plans/security-module.md), and [roadmap](ROADMAP.md). `ConsumeSandboxPlan2` carries the original typed `Plan2` envelope as well as the diff --git a/docs/README.md b/docs/README.md index 0c0f2b16..09dd3234 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,8 @@ docs/ - **[`specs/`](specs/README.md)** — one file per feature, module, or subsystem, describing its behavior, public surface, invariants, and acceptance criteria. +- **[`module-crypto.md`](module-crypto.md)** — confidential device, master, cipher, password, keyring and pairing members of the TinySecurity module contract. +- **[`module-subject-policies.md`](module-subject-policies.md)** — registered subject policy members and their generation-bound authorization rules. - **[`plans/`](plans/README.md)** — implementation-ordered, test-first steps for delivering an approved specification. Plans name exact files and verification commands, and are updated as the work progresses. diff --git a/docs/module-crypto.md b/docs/module-crypto.md new file mode 100644 index 00000000..b5457b68 --- /dev/null +++ b/docs/module-crypto.md @@ -0,0 +1,173 @@ +# Confidential crypto and secret members + +## Confidential device handles + +Optional `ModuleConfig.device: DeviceConfig2` adds `EvaluateDevice2` and +`ExecuteDevice2` to the native interface. Existing omitted-device payloads and +v2/v3/v4 wire frames keep their serialized forms. These members require TinyBus +confidential transport and the immutable verified `trusted_host` peer. Missing +configuration, another sender, public identity claims, unconfidential calls, +missing callbacks and required audit failures deny native authority. + +Each configured device scope references a subject already registered in +`approvals`, captures its verified origin, and optionally consumes one static +X25519 credential during SDK init. Absent credentials generate native static +keys. Neither variant exports private material. Device calls select only an +existing scope and a bounded operation: issue a static/ephemeral key handle, +derive a role-bound tunnel, seal/open v2, seal/open v3 handshake or v4 ack, +revoke a handle, or cancel the authenticated scope. Public keys retain the +canonical 32-byte URL-safe unpadded base64 format. + +Evaluate builds the exact content digest internally and uses the existing +verified approval callbacks. Execute requires the resulting exact human Once +receipt and fresh required audit correlation; automatic, durable-tool or +caller-asserted decisions cannot issue native authority. Every callback/worker +await is followed by live generation/scope/deadline checks. Plaintext, keys, +frames and handles do not reach approval/audit payloads, errors, Debug or +`PolicyInfo`. + +Explicit init bounds cover registrations, handles, concurrent reservations, +per-scope rate, TTL, message bytes and retained bootstrap nonces. Reinit may +change bounds without reimporting credentials or changing immutable subjects. +It invalidates all old handles; successful new-generation receipts issue new +handles. Revoke removes dependent tunnels; scoped Cancel fences pending work. +Bootstrap replay evidence survives alias issuance, Cancel and reinit and fails +closed at capacity. Clock rollback/unavailability faults device authority for +that process. Shutdown closes owned credentials even when an SDK observer +retains the connection. See the [owned cleanup and replay limits](../crates/tinysecurity-crypto/README.md#scoped-device-handles). + +`device_native_contract` loads the hashed shared library and exercises actual +confidential dispatch, verified approval/audit callback continuity, v2/v3/v4, +replay/revoke, valid generation-two receipts, old-handle denial and SDK shutdown. +Run the retained runner with the absolute library path from both owner and +non-owner working directories. Linux proves Linux behavior; macOS and Windows +require their native runners. This does not release artifacts or migrate a host. + +## Confidential injected master resolution + +Contract 1.10 implements optional `ModuleConfig.secret` and confidential +`EvaluateSecret2` / `ExecuteSecret2`. The only operations are +`ResolveConfigMaster` and `ResolveAppMaster`. Init registers metadata: an exact +existing approval scope, purpose, opaque backend descriptor, target named slot +and at most three compatible historical readers. App authority is separately +registered; calls cannot override subjects, origins, backends, slots or paths. + +Evaluation retains the same bounded native reservation, private challenge and +keyed safe correlation through execution. Registered auto approval, reusable +choices and judges cannot issue this grant. Actual confidential `VerifyHuman`, +Human Once decision and reservation CAS, and the exact required audit sink +receipt finish before backend load or mint. A canceled begin holds its admitted +reservation until expiry; retry requires a fresh original call id. There is no +second wait queue. + +The fixed `SecretBackend2` callback has confidential `Load` and `Set` only. +Every reply echoes backend, registration, generation, operation id, phase and +slot before status interpretation. Denial is not absence. Encodings are bounded +to 4096 bytes. A valid target skips all historical readers; compatible sources +must parse and agree before mutation. Their original uppercase/whitespace +encoding is copied unchanged and verified by exact target readback. No source +is deleted. File is only a named injected store entry, not physical file access. + +One resolver and exclusive callback context persist for each immutable binding. +Live native authority is checked before and after each await and before mint +and publication. Cancellation after Set begins, timeout, wrong readback and +indeterminate mutation permanently fence that resolver. The host callback broker +must independently serialize each actual target slot, retain detached provider +workers and block fresh reads/mint/Set until trusted quiescence and exact readback. +Operation ids and Stored acknowledgements do not provide generic CAS or durability. +Recreating a resolver, restarting a process or dropping a service tree is not +recovery authority. This checkpoint provides no recovery member. + +Results contain a bounded random alias, purpose and generation only. The master +and underlying native handle remain internal, with no alias-use/read/export +member. Aliases are pruned on expiry or revocation and cleared at close. Owned +successfully decoded callback values and unpublished output are wiped; SDK +serialization, frame copies and partially decoded serde values are outside that +cleanup guarantee. + +Configured-secret SDK reinit keeps the same process owner, store brokers and +master resolvers. Native and device transitions retain their state locks through +policy publication; valid-clock publication rejection preserves old authority. +Accepted generations invalidate pending native work and clear aliases, while +persistent resolver uncertainty remains unavailable. Native clock faults still +close authority permanently. Device replay capacity and every scope epoch are +checked before publication; bootstrap evidence survives accepted reinit. Physical files, plaintext delivery and routine native consumers remain outside +this subset; password, OS keyring and pairing members are in contract 1.14 below +(`PasswordCrypt2`, `KeyringAccess2`, `PairingControl2`). + +## Confidential native config cipher (contract 1.11) + +`EvaluateSecretCipher2` and `ExecuteSecretCipher2` are confidential routine +operations over an actual `ExecuteSecret2` ConfigUserMaster acquisition. They +never prompt for routine fields or create master authority from a receipt. +`secret_cipher` SDK metadata fixes one existing registration, consumer identity, +enabled setting, callback timeout and physical slot count. It cannot change on +reinit. Older 1.10 initialization omits it and retains its original behavior. +An older decoder does not know the additive `native_routine` audit kind; hosts +using this capability must negotiate the new contract vocabulary. + +Evaluate owns the real private crypto pending reservation, then commits required +sanitized NativeRoutine intent through the existing AuditClient. Execute removes +it once, requires unchanged metadata and confidential input, and checks actual +execution, output-stage and publication audit acknowledgements. Audit arguments +contain keyed operation/output correlations, Engine-derived durable scope/policy +and fixed classifications. Caller-supplied audit event identities reserve stream +order; they never construct a private routine acknowledgement or cipher grant. + +Plaintext, disabled/empty passthrough and disabled migration replacements go only +to the fixed confidential `SecretCipherConsumer2.Deliver` port. A real trusted +receiver authenticates the pinned loaded module and stages zeroizing output in +an exclusive bounded physical slot. It refuses overwrite and releases locally +only after the matching successful owner response. A staged callback reply is +not product plaintext publication. The owner retains its original reservation +through final required audit and live validation; only true ciphertext appears +in ordinary result fields. A failed/canceled/late/invalid stage leaves its sender +slot permanently occupied across generation changes and reinit. Operation-ID +or status echoes cannot clear uncertainty. A successful response permits the +receiver to consume/wipe its exact slot before accepting another stage there. + +The broker currently requires every confidential destination to be an admitted +native module. Ordinary unattested host callbacks are denied, including master +HumanOnly prompt and secret backend delivery. The tracked native fixture loads +the real hashed TinySecurity cdylib and a separately admitted host-trusted native +callback fixture. This proves module-to-admitted-native-consumer composition; +it does not prove ordinary OpenHuman host callback admission. That TinyBus +integration gap remains open. SDK serde/frame/partial-decode copies and process +abort are outside the owned-buffer wiping guarantee. App document storage remains a separate unfinished capability. + +## Confidential password, keyring and pairing members (contract 1.14) + +Optional `ModuleConfig.crypto: CryptoConfig2` adds three confidential members, +`PasswordCrypt2`, `KeyringAccess2` and `PairingControl2`. Like the device and +secret members they require TinyBus confidential transport and the immutable +verified `trusted_host` peer; an unconfigured module denies all three. Config is +init-only and immutable across reinit. It bounds payload bytes (at most 1 MiB) +and operations per monotonic minute (at most 4096). + +`PasswordCrypt2` preserves the existing Argon2id v0x13 (64 MiB, 3 passes, 1 lane) +plus AES-256-GCM format. The payload keeps the legacy byte-array shape +(`ciphertext`, `nonce`, `salt`); encrypt generates a sixteen-byte salt unless the +caller supplies one for the legacy separate-salt layout. Frozen host fixtures +decrypt unchanged. At most two derivations run at once and there is no wait queue. + +`KeyringAccess2` reads, writes or deletes a named secret on a backend chosen at +init: platform default (Keychain, Credential Manager, Linux kernel keyring), +Linux kernel keyring, or a protected encrypted file in an absolute directory. +Linux Secret Service is not compiled into this module and fails closed. Every +operation first calls the host's `ai.tinyhumans.tinysecurity.KeyringConsent2` +`Consent` callback with the verified caller, purpose, access kind and secret name +(never the value), and proceeds only on an explicit allow. Invalid names or +purposes, oversized values and denied or timed-out consent never reach the backend. + +`PairingControl2` exposes `Status`, `Authenticate` (constant-time bearer check +against legacy tokens), `Invalidate` and `Revoke` for accounts registered at init. +Challenge issuance and numeric verification (`Begin`/`Verify`) are not exposed: +they need confidential delivery to an admitted native destination, the same open +TinyBus gap as the config cipher consumer. Their ports fail closed. + +Passwords, plaintext, secret values and bearers are wiped after use and never +appear in `Debug`, errors, `PolicyInfo` (only `callbacks.crypto`) or audit rows. +The X25519/HKDF device tunnel remains the `EvaluateDevice2`/`ExecuteDevice2` +surface above. The OS keyring test runs on every platform and is required to +use the real credential store when `TINYSECURITY_REQUIRE_OS_KEYRING=1`; it +always removes its entry. diff --git a/docs/module-subject-policies.md b/docs/module-subject-policies.md new file mode 100644 index 00000000..7480c417 --- /dev/null +++ b/docs/module-subject-policies.md @@ -0,0 +1,51 @@ +# Registered subject policies + +## Registered subject policies (contract 1.12, extended in 1.13) + +`RegisterPolicy(RegisteredPolicy)` returns an opaque `PolicyId` binding one +verified subject to a registered path scope, a registered command scope, +nonsensitive `PolicySettings` (base policy, privacy, egress hosts, permitted +command classes, auto-approve origins, judge opt-in) and ordered `ToolRule`s. +Registering the same value again returns the same id; a different value for an +active subject is rejected and must go through `SupersedePolicy`. Ids are private +references, not credentials, and registration is a trusted-host operation. + +`EvaluateScoped` and `CheckScoped` take a `PolicyId`, the verified +`CallerContext` and the original call or checks. The caller's agent must equal +the registered subject, so one tenant cannot use another's id. Decisions carry +the module generation, the policy's lineage generation and a SHA-256 call +fingerprint over the policy id, caller (excluding only the call instance) and +the original call. The fingerprint correlates and binds a decision to its call; +it does not make the decision reusable by itself. Hosts may reuse a decision only +when its `cacheable` flag is true. Scoped decisions are cacheable for +`LocalDiagnostic` and `Path` checks and for tool-rule denials, and never for command +checks (they reserve budget), foreign or retired callers, or audited members; key a +cache on the whole fingerprint plus both generations. The registered filesystem scope +applies on top of the base settings, and commands run through the registered command +scope, so budgets and classes are per subject. A superseded id denies with `invalid_context`. Unbound scoped +members return `approval_unavailable` for permitted work while required audit is +configured; `EvaluateScopedAudited` and `CheckScopedAudited` (1.13) take the same +`AuditedRequest2` wrapper as `EvaluateAudited2`/`CheckAudited2` and are the permitted +path under required audit. `PolicySettings.policy.forbidden_paths` accepts `~`, +relative (resolved against `action_dir`) and floor entries; see +[`specs/scoped-policies.md`](specs/scoped-policies.md). + +`SupersedePolicy` replaces a subject's active policy atomically; the old policy +stays active on any failure. If the replacement widens authority (new or broader +roots, removed restrictions, new hosts or tools, higher command autonomy, +classes, allowlist entries or budget, a command scope or execution +context that grants more (a different scope id alone is not a widening), new auto origins, judge enabled, +enforcement or the registered path scope disabled, or privacy loosened) the response is `ReviewRequired` with the exact +`policy_change` call. The host passes it to `EvaluateApproval2` under the old +policy's approval scope; after a human decision it repeats the request with +`PolicyChangeApproval`, which reserves the approval once before activation. Only a +row decided by an authenticated human qualifies; automatic, judge and reused +approvals are refused. A +narrowing change activates without review. Committed activation emits `PolicyChanged` +carrying the new `policy_id` and `policy_generation`. + +`PolicyInfo` adds nonsensitive judge settings, callback and authority +availability, and the active scoped-policy count; `ScopedPolicyInfo(PolicyId)` +returns one policy's effective settings and rules. No credential, endpoint or +scope handle appears in either. `max_cost_per_day_cents` is not part of the +contract: see ADR 0006. From 435a6d06e0af320aadcf0dc6af326965c09a01e3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 18 Oct 2026 22:44:00 +0300 Subject: [PATCH 4/5] fix(ci): skip Rustdoc in changed executable coverage --- .github/scripts/check-native-coverage.py | 27 +++++++++++++++---- .../scripts/check-native-coverage_tests.py | 9 +++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/scripts/check-native-coverage.py b/.github/scripts/check-native-coverage.py index 86d05366..6fe4dfd8 100644 --- a/.github/scripts/check-native-coverage.py +++ b/.github/scripts/check-native-coverage.py @@ -110,9 +110,11 @@ def production_source(name): def changed_lines(diff): - """Extract added/modified production line ranges from git's unified=0 diff.""" + """Extract changed executable production lines from git's unified=0 diff.""" files = {} current = None + new_line = None + remaining = 0 for line in diff.splitlines(): if line.startswith('+++ '): name = line[4:] @@ -123,14 +125,29 @@ def changed_lines(diff): raise ValueError('invalid git diff source path') if current is not None and not production_source(current): current = None + new_line = None + remaining = 0 elif current is not None and line.startswith('@@ '): match = re.match(r'@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line) if match is None: raise ValueError('invalid git diff hunk') - first = int(match[1]) - count = int(match[2]) if match[2] is not None else 1 - if count: - files.setdefault(current, set()).update(range(first, first + count)) + new_line = int(match[1]) + remaining = int(match[2]) if match[2] is not None else 1 + elif current is not None and new_line is not None and remaining: + if line.startswith('+') and not line.startswith('+++'): + source = line[1:].lstrip() + # Rustdoc comments do not compile to executable lines. In + # particular, documentation-only changes must not fail the + # changed-line gate for having no measured executable lines. + if not source.startswith(('///', '//!')): + files.setdefault(current, set()).add(new_line) + new_line += 1 + remaining -= 1 + elif line.startswith('-') and not line.startswith('---'): + continue + elif line.startswith(' '): + new_line += 1 + remaining -= 1 return files diff --git a/.github/scripts/check-native-coverage_tests.py b/.github/scripts/check-native-coverage_tests.py index 4848777e..883271b4 100644 --- a/.github/scripts/check-native-coverage_tests.py +++ b/.github/scripts/check-native-coverage_tests.py @@ -83,6 +83,15 @@ def test_utf8_git_diff_is_measured_under_windows_charmap_locale(self): self.assertIn('Changed executable lines: 1/1 (100.00%)', output.getvalue()) self.assertNotIn('No changed production', output.getvalue()) + def test_rustdoc_only_changes_skip_changed_executable_line_gate(self): + output = io.StringIO() + with self.git_coverage_fixture(b'/// updated documentation\n') as argv, \ + mock.patch.object(sys, 'argv', argv), \ + contextlib.redirect_stdout(output): + gate.main() + self.assertIn('No changed compiled executable sources', output.getvalue()) + self.assertNotIn('Changed executable lines:', output.getvalue()) + def test_malformed_utf8_git_diff_is_rejected_instead_of_skipping_changes(self): output, errors = io.StringIO(), io.StringIO() with self.git_coverage_fixture(b'// invalid \xff\n') as argv, \ From 308c9e59040c043341494d7c14cf92e7d0488d44 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 18 Oct 2026 22:45:06 +0300 Subject: [PATCH 5/5] docs: restore confidential master anchor --- MODULE.md | 3 +++ README.md | 2 +- docs/specs/security-module.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/MODULE.md b/MODULE.md index 3a4bb14c..fa957a5d 100644 --- a/MODULE.md +++ b/MODULE.md @@ -121,6 +121,9 @@ the [security architecture page](https://github.com/tinyhumansai/openhuman/blob/ ## Confidential device handles Full details: [`Confidential device handles`](docs/module-crypto.md#confidential-device-handles). +## Confidential injected master resolution +Full details: [`Confidential injected master resolution`](docs/module-crypto.md#confidential-injected-master-resolution). + ## Registered subject policies (contract 1.12, extended in 1.13) Full details: [`Registered subject policies (contract 1.12, extended in 1.13)`](docs/module-subject-policies.md#registered-subject-policies-contract-112-extended-in-113). diff --git a/README.md b/README.md index 0191b33b..490afc03 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ scores meet local thresholds before durable `auto:jev` one-shot approval; all uncertain/error/budget paths keep the authenticated human flow. Required CAS/audit receipts and fresh policy floors still gate permission. -Contract 1.10 adds [confidential injected master resolution](MODULE.md#confidential-injected-master-resolution) with opaque native aliases. Configured-secret reinit is conservatively denied pending a coherent owner transaction. This is an injected-store prerequisite, not production keyring or password migration. +Contract 1.10 adds [confidential injected master resolution](docs/module-crypto.md#confidential-injected-master-resolution) with opaque native aliases. Configured-secret reinit is conservatively denied pending a coherent owner transaction. This is an injected-store prerequisite, not production keyring or password migration. Contract 1.9 adds [scoped declarative sandbox planning](crates/tinysecurity-sandbox/README.md). `Plan2` uses immutable host-registered source provenance, ceilings and executor diff --git a/docs/specs/security-module.md b/docs/specs/security-module.md index 434ecb99..57485334 100644 --- a/docs/specs/security-module.md +++ b/docs/specs/security-module.md @@ -337,7 +337,7 @@ The owner validates its canonical binding before audit and again after the callback; changed operations cannot consume a copied artifact. This does not replace TinyBox's live isolation and capability checks. -The [contract 1.10 master subset](../../MODULE.md#confidential-injected-master-resolution) +The [contract 1.10 master subset](../module-crypto.md#confidential-injected-master-resolution) implements only confidential config/app master resolution through immutable injected callbacks, exact Human Once consumption and opaque native aliases. Per-binding resolver uncertainty persists for the supplied broker lifetime across accepted