Skip to main content

Runner extensions

Build a custom Rust executable around the tenon library to supply these hooks:

HookControlsDefault
HttpApiAuthorizationAccess to HTTP requestsNoHttpAuth
ExecutionPolicyWhich Tenon Documents may run and for how longAllowAll
DocumentProtectionTransformation of complete saved Tenon Document bytesPlaintext

Implement the policy, credentials, and any encryption in your executable. Tenon does not supply a licensing or billing service.

The security policy and threat model describes the limits of each hook and the responsibilities of a custom distribution. Hook availability alone does not enable access control or turn plugin processes into security sandboxes.

fn main() -> std::process::ExitCode {
tenon::run_main_with(|_config| Ok(tenon::RunnerHooks::default()))
}

run_main_with calls your initializer once with validated RunnerConfig, before loading saved Tenon Documents or accepting HTTP requests. It returns ExitCode.

Put custom settings in extra and sanitize any initialization errors.

RunnerHooks::new(policy, protection, authorization) installs concrete implementations. Defaults are AllowAll, Plaintext and NoHttpAuth.

DocumentProtection​

DocumentProtection: Send + Sync exposes synchronous protect(source: &[u8], output: &mut dyn Write) and unprotect(stored: &[u8], output: &mut dyn Write), both returning io::Result<()>. Implement the byte transformation; Tenon handles file storage.

A failed protect leaves the previous saved Tenon Document intact. A failed unprotect prevents startup and preserves the file. Methods may be called concurrently for different Tenon Documents. Bound their memory and computation costs; cancellation does not roll back a file operation already underway.

ETags, GET responses and plugin configuration always use original plaintext bytes. Randomized protected bytes do not change the ETag. Storage protection is not API authorization, field-level secret expansion or encryption of runtime memory.

ExecutionPolicy​

authorize(ExecutionScope) synchronously receives the complete proposed set of admitted, statically verified Tenon Documents and returns Result<ExecutionPermit, ExecutionDenied>. Scope references are borrowed for that call and have no ordering guarantee. Quota decisions use the proposed Tenon Document set. Missing-plugin Tenon Documents can still consume admission quota.

Calls happen on the Runner thread; the policy need not be Send/Sync and must return promptly without blocking I/O. Do not maintain an independent quota ledger by counting callbacks. An allowed Tenon Document can still fail to save.

OperationAdmission behavior
StartupTries saved Tenon Documents in ID byte order
PUTChecks the complete proposed set after validation and write conditions, before persistence
Denied PUTReturns 403 with the hook's sanitized code and message
Denied saved Tenon DocumentRemains readable and deletable, without an admitted pipeline; an identical PUT retries admission
DELETEFrees desired quota without another policy call
Plugin install, automatic restart, reconfigurationDoes not reauthorize unchanged desired configuration

Both accepted and denied results carry entitlement_until: Option<Instant>:

  • Every result immediately replaces the deadline; None removes it, even if a later file operation fails.
  • Expiry starts bounded Runner shutdown, including when there is no API traffic. Process termination is not instantaneous.
  • Once shutdown starts, later results cannot reopen admission.

Update policy inputs in your executable. Authorization runs only during startup admission and Tenon Document PUT requests.

HttpApiAuthorization​

HttpApiAuthorization: Send + Sync returns a Send future of Result<(), HttpAuthRejection>.

Request informationAvailable to the hook?
Original method, URI path, headersYes, including unknown methods and routes
Query, body, trailers, TLS client identityNo

Checks run before endpoint body processing. Decode each path segment once when interpreting resource IDs; paths retain percent encoding. Preserve repeated and non-text headers.

Unauthorized requires a WWW-Authenticate challenge and yields 401; Forbidden yields 403 with an optional challenge. Core responses contain stable public messages and do not echo credentials or internal errors. HEAD responses have no body. Denial does not modify execution admission, entitlement deadlines or running Pipelines.

  • Await external services without blocking executor threads; implement timeouts and failure handling.
  • Release held resources if cancellation drops the future. An abandoned callback may never complete.
  • Requests rejected during shutdown receive 503 without invoking the hook.
  • With mutual TLS, transport authentication happens first.
  • Each new diagnostic stream is authorized. An existing stream is not continuously reauthenticated.