W-137 S-3 (CTX-0003) Lua policy package for the Bitty history keeper:
opt-in history reads over the public host API only. Accepted contracts are
RFC-0004 (history-read surface, bitty-docs), W-131 (storage-and-history
boundary, bitty-docs), and W-137 (plugin history-and-storage policy,
bitty-plugins-docs); the SDK binding is bitty-plugin-sdk at 3ea8e58
(merged main: history surface from PR #144, declared-settings support from
PR #152).
History answers bounded, read-only snapshot queries over Bitty's persisted
history: sealed transcript segments, command history, and the plugin's own
key-value namespace. Every answer is redacted, truncated, attributed, and
labeled untrusted-observation, or replaced by a typed denial. The plugin
never streams, holds no cursor across calls, and never reads session
snapshots.
Prerequisites:
- Bitty
>=0.0.24with Plugin API^1.0(the manifest[compat]minimum; no upper bound is declared). - Opt-in capture enabled for the sources you query. Transcript and command
history are unavailable when capture is off
(
E_HISTORY_CAPTURE_DISABLED). - Consent to the three read grants:
history.transcript.read,history.commands.read, andhistory.kv.read. The plugin has no export authority, so reading never implies clipboard, filesystem, or process access.
Install from a local checkout:
git clone https://github.com/bitty-terminal/history.git
cd history
bitty plugin install "$PWD"
bitty plugin enable bitty-terminal.history
bitty plugin listbitty plugin install <path> stages the package under the XDG data store and
records the capabilities you consent to; it executes no plugin code
(--yes approves non-interactively). Verify activation with
bitty plugin list: the entry must show bitty-terminal.history, version
0.0.1, source local-path, enabled, and the three read grants.
bitty plugin info bitty-terminal.history prints the recorded grant set.
Every command id is qualified with the plugin id. All commands return a table
with ok and code; on success the bounded page is delivered through the
host, and on denial code carries the typed error. None of the commands has a
default keybinding.
| Command | What it does | Arguments | Default keybinding |
|---|---|---|---|
bitty-terminal.history:query-transcript |
One bounded snapshot over sealed transcript segments while opt-in capture holds. | scope (panel and/or workspace), row_start (integer ≥ 0), row_count (integer ≥ 1, cap 16), max_bytes (integer ≥ 1, cap 4096), op (list default, tail, or search), needle (1–256 bytes, required for search). |
None |
bitty-terminal.history:query-commands |
One bounded list, tail, or search read over command history. | Same as query-transcript. |
None |
bitty-terminal.history:query-kv |
One bounded snapshot read of the plugin's own KV namespace; cross-plugin reads are unrepresentable. | row_start, row_count (cap 16), max_bytes (cap 4096), op, needle. No panel or workspace extent: the caller namespace is the scope. |
None |
Explicit arguments always win. An argument-free dispatch (a keybinding or
palette invocation carrying {}) resolves the bounded defaults from
Configuration; with no usable scope axis it denies with
E_HISTORY_SCOPE_MISMATCH rather than widening the query.
Settings live in $XDG_CONFIG_HOME/bitty/plugins/bitty-terminal.history.lua.
Values are read at activation and pinned to that plugin generation.
| Key | Type | Default | Description | Reload |
|---|---|---|---|---|
default_panel |
string |
"" |
Panel id used as the default scope axis when a dispatch carries no explicit scope. Empty means unset; wildcard, blank, control, or over-long ids fail as E_DEF_INVALID. |
Restart |
default_workspace |
string |
"" |
Workspace id used as the default scope axis when a dispatch carries no explicit scope. Empty means unset; the same validation applies. | Restart |
row_count |
int |
16 |
Rows requested when a dispatch carries no explicit row_count. Accepted 1–16; an out-of-window value falls back to the shipped cap (16). |
Restart |
max_bytes |
int |
4096 |
Byte budget when a dispatch carries no explicit max_bytes. Accepted 1–4096; an out-of-window value falls back to the shipped cap (4096). |
Restart |
Example:
-- $XDG_CONFIG_HOME/bitty/plugins/bitty-terminal.history.lua
return {
default_panel = "", -- set to a panel id for argument-free dispatch
default_workspace = "",
row_count = 16,
max_bytes = 4096,
}Restart means settings are resolved during activation: there is no settings
watcher or hot-reload path, so edit the file and restart Bitty. Explicit
scope, row_count, and max_bytes arguments override these values.
Configured values are convenience defaults, never authority: the host still
intersects every query scope with the granted scope and denies over-bound
requests.
History registers no default keybindings. Bind a chord in
$XDG_CONFIG_HOME/bitty/init.lua:
return {
keymaps = {
{
chord = "ctrl+shift+h",
action = "bitty-terminal.history:query-transcript",
context = "global",
},
},
}An argument-free binding uses the Configuration defaults above. To query a
specific panel or workspace, bind the command through a caller that passes
scope explicitly.
| Code | Class | Cause | Remedy |
|---|---|---|---|
E_SETTINGS_UNDECLARED |
validation |
A settings read named a key absent from the manifest [settings] declarations. |
Install a plugin version whose declarations match the running build. Only the four keys above are declared; undeclared config entries are ignored. |
E_INCOMPATIBLE |
validation |
The running host does not satisfy >=0.0.24, or Plugin API is not ^1.0. |
Upgrade Bitty or install a History release whose [compat] minimum covers your host. |
E_CAPABILITY_DENIED |
runtime |
A required read grant is missing, expired, or revoked. | Re-run bitty plugin install <path> and approve the current grant set; bitty plugin list shows what is recorded. |
E_HISTORY_MISSING_GRANT |
runtime |
No per-plugin, per-source read grant is recorded (deny by default). | Approve the matching history.*.read grant for the source you query. |
E_HISTORY_REVOKED_GRANT |
runtime |
The grant existed but was revoked or expired after activation. | Re-grant it and restart Bitty; a live generation keeps the grant bound at activation. |
E_HISTORY_SCOPE_MISMATCH |
runtime |
The query names no panel/workspace extent, the grant does not cover it, or no usable default context is configured. | Pass an explicit scope, or set default_panel/default_workspace and restart. The host only returns data inside the granted scope. |
E_HISTORY_OVER_BOUND |
runtime |
row_count, max_bytes, or the needle exceeds the accepted cap, or a bound is zero. |
Stay inside 1–16 rows, 1–4096 bytes, and a 1–256 byte needle. The plugin never clamps an explicit over-bound request. |
E_HISTORY_CAPTURE_DISABLED |
runtime |
Opt-in capture for the source is off, so nothing was persisted to read. | Enable capture for transcript or command history, then query again. |
E_HISTORY_SAFE_MODE |
runtime |
Bitty runs in safe mode, which reads no transcript, history, snapshot, or KV. | Restart without safe mode to use History. |
E_HISTORY_TRUST_DENIED |
runtime |
The caller's trust level or domain admits no standing access. | Query from a trusted local context; unknown trust levels and domains deny rather than default. |
E_HISTORY_UNAVAILABLE |
runtime |
The content was purged or expired, or the source is otherwise unavailable. | Treat it as typed unavailability, not an empty result. Query a narrower point in time or accept that the data is gone. |
E_DEF_INVALID |
validation |
A configured default scope is wildcard, blank, control-bearing, or longer than 128 bytes. | Fix default_panel or default_workspace and restart. Empty string means "no bound context", not a wildcard. |
E_DISABLED means the activation-time compatibility gate rejected this
generation, and NO_HOST means the module was loaded without the host bridge
for tests or headless use; neither state exposes commands.
bitty-plugin.toml: plugin idbitty-terminal.history, version0.0.1, compat floorbitty >=0.0.24withplugin-api ^1.0and no upper bound, exactly three capabilities (history.transcript.read,history.commands.read,history.kv.read), lazyquery-transcript/query-commands/query-kvcommands, no event subscriptions, and four declared settings (default_panel,default_workspace,row_count,max_bytes) that supply capability-neutral bounded defaults. No export capability: per the RFC-0004 grant-combination rule the history grant never implies clipboard, filesystem, or process authority.lua/history/init.lua: the whole policy. Source/scope/bound validation (transcript, commands, own KV; session snapshots fail closed), one-shot bounded snapshot queries throughbitty.history.*.query, typed-denial surfacing with oracle-tight shapes, and untrusted-label verification on every delivered page (redacted, labeled, attributed, point-in-time). No streaming, no watch, no tail-follow; no session-snapshot access.tests/: bun + wasmoon suite against the SDKMockHost, mirroring the composer harness pattern. Covers scoped snapshots with typed denials, source isolation, tail/search ops, over-bound denials, purge/expiry as typed unavailability, safe-mode and trust denials, revoked-vs-missing distinction, one-shot command dispatch, argument-free keybinding dispatch with bounded defaults (explicit-scope precedence and the no-context denial), mismatch disable, denial parity vs a foreign manifest, and oracle-tight denial texts.
- Public API only. No filesystem, process-spawn, network, clipboard, terminal-input, or selection-copy authority; reading authorizes delivery into the plugin VM only and export stays separately granted.
- Explicit scope and bound on every read: transcript/commands need a
panel and/or workspace extent (never
*orall), KV takes no extent; every query carries explicitrow_count/max_bytesover zero. Over-bound requests deny; the plugin never clamps silently. - Argument-free dispatch (a keybinding or palette invocation carrying
{}) resolves a bounded default instead of failing. Explicitscope,row_count, andmax_bytesalways win unchanged. When they are absent the plugin reads the declared settings context:default_panel/default_workspace(empty string means unset; wildcard, blank, control, or over-long ids fail closed asE_DEF_INVALID) androw_count/max_bytes(accepted only inside the 1..16 / 1..4096 caps; an out-of-window configured value falls back to the shipped cap). With no valid scope axis the query still denies withE_HISTORY_SCOPE_MISMATCH. The public Lua surface exposes no focus getter, so the plugin never readsbitty.workspace/bitty.paneland never widens its capability set to resolve one; the host remains authoritative and still intersects every query scope with the granted scope. - Records are redacted and truncated with attribution and the
Core-attached
untrusted-observationlabel: bodies are content under the prompt-injection rule, never instructions, never routed into an instruction channel. - Purges and expiry are respected: purged content surfaces as typed unavailability, never as a silent gap and never resurrected from a derived index.
- Version/capability mismatch disables with a diagnostic and no partial activation: no commands exist for the generation.
- Bounds mirror the accepted shape (16 rows / 4096 bytes per query, 256 bytes per row, 1..256 byte needles, 1..128 byte scope ids): the shape is normative, exact numbers stay parked to W-137/W-139/W-146.
- No Core changes, no host-API implementation, no SDK changes.
- No registry onboarding and no release; the package stays a candidate until CTX-0004 independently verifies privacy, retention, quota, and parity evidence.
- No second read channel: the plugin registers no event subscription and holds no cursor across calls; a caller that wants newer state issues a new bounded query under its grant.
The host validates plugin-api ^1.0 before activation and fails closed on
mismatch. Host compatibility is minimum-only: [compat].bitty declares the
verified floor >=0.0.24 and no ceiling, so newer hosts install without a
manifest edit. The plugin repeats the major-version gate at load as
defense in depth: on mismatch it records disabled_reason and registers
nothing for the generation.
RFC-0004 accepted + threat-model 48b60c4 + W-131 accepted (bitty-docs
21d63dc) + W-137 accepted (bitty-plugins-docs 002c7ce) + Core host
76fa42d6; SDK 3ea8e58 (merged main, history + declared settings).
Opt-in bounded capture, deletion/retention, privacy and recovery; Atuin
through supported CLI/API only. Do not treat OSC133 as command/cwd proof.
CTX-0001 -> CTX-0002 -> CTX-0003 -> CTX-0004 maps to Issues 4 -> 3 -> 2 -> 1. CTX-0001 (bootstrap) is complete, CTX-0002 (contract readiness) is accepted, CTX-0003 implementation is this package, and CTX-0004 independent verification is recorded; CTX-0005 adds the bounded default context for argument-free dispatch.
Run the repository gates through the justfile:
just install
just checkjust check includes just docs-check, which fails when the README
Configuration table omits a declared [settings] key, documents an undeclared
key, or drifts from the declared type or default.