lock family docs GitHub

Changelog · Changelogs

lockkit changelog

lockkit has no stability promise before v1: pin an exact version. GUIDE.md is the contract between lockkit and the products built on it.

v0.19.2 (2026-11-14) #

Changes #

  • pii: LooksPersonNameColumn reads German plurals in -er (mitglieder) and staedte/städte as table hints. The detection bench scores it on sampled name columns (namecolumns.jsonl: name/full_name/nom/naam with EN/FR/NL/DE names, and products/roles/cities/tags negatives), held to the baseline like the detectors (#163).
  • pii: a bare stop word (and, or, to…) after a secret command-line flag is no longer masked (use --password or --token), and a flag secret only through auth (--auth, --auth-local, --auth-host) keeps an auth method name (peer, scram-sha-256, md5…): initdb's --auth-local and --auth-host hint and --auth-local peer stay readable; --auth admin:pw, --auth-token …, --auth s3cr3tT0ken and --password hunter2 stay masked (#198).

v0.19.1 (2026-11-13) #

Changes #

  • pii: the email, iban, card, be_niss, fr_nir, nl_bsn, us_ssn and ip finders skip their regexp or scan when a byte-level check shows the text cannot hold a match (no @; too few digits; no letter-letter-digit-digit run; too few : or .). Results are unchanged (an exactness test and fuzz target compare each finder with and without its prefilter); FindInViews with AllDetectors on short log messages runs about 3x faster (#195).

v0.19.0 (2026-11-13) #

Behaviour changes #

Masking with the default detectors (AllDetectors, or a list naming phone, fr_nir, card, be_niss, email) can mask more, or under another kind, after a bump; the new detectors are all opt-in (#130).

  • pii: fr_nir now runs before card: a French NIR whose digits pass Luhn is masked as a NIR, no longer as a card number.
  • pii: phone without phone_loose also finds a national number after a phone word (tel 612 345 678), and 12-digit German mobiles (0151 23456789); be_niss finds a valid number with other separators after NISS/rijksregister; an e-mail local part may hold an apostrophe (o'hara).
  • pii: Match has a new Confidence field; masked output is unchanged.
  • managed: File.Decode into a map[string]any (or an any) now takes nested tables where it failed with unknown key (#192). Struct targets stay strict.

Changes #

  • managed: File.Decode into a map[string]any (or an any, or a struct with such a field) now takes a section holding nested tables: inline tables, sub-tables, tables nested deeper and arrays of tables no longer fail with unknown key "limits.x". Struct targets stay strict: a key a struct does not declare is still refused, nested structs included. Closes #192.
  • pii: new opt-in detectors plate (BE, FR and Dutch sidecode 4-11 licence plates, upper case with dashes) and postal_address (BE, FR, NL and English street addresses with their postcode and locality), both pattern only (medium). Gaps the bench found: fr_nir now runs before card, so a NIR whose digits pass Luhn is a NIR (fr recall 0.783 to 1.000; AllDetectors keeps the same detectors in that new order); phone finds 12-digit German mobiles (0151 23456789); an e-mail local part may hold an apostrophe (o'hara). Closes #130.
  • pii: new opt-in detectors be_eid (Belgian identity card numbers, ddd-ddddddd-dd, mod 97, high confidence), and the context-only passport (Belgian passport numbers after a passport word) and birth_date (a date after a birth word or in a birth field, never a date alone), both low confidence. Normalise gives a birth date as YYYY-MM-DD whatever its spelling (#130).
  • pii: new opt-in detectors eu_vat (BE, FR, NL, DE, LU VAT numbers, checksums verified) and be_company (Belgian KBO/BCE company numbers), run after iban and before card; Normalise keeps their ASCII letters and digits, upper case. Match has a Confidence (ConfidenceHigh for a verified checksum, ConfidenceMedium for a pattern, ConfidenceLow for a gazetteer or context match); masked output is unchanged. Context words raise weak candidates to low confidence matches, additively: phone (without phone_loose) now also finds tel 612 345 678, be_niss a valid number with other separators after NISS/rijksregister, be_company ten bare digits after KBO/BCE. New FindField(field, s, ds) uses the field name as that context. AllDetectors is unchanged. Bench corpus extended (appended blocks) and baseline updated (#130).

v0.18.0 (2026-11-13) #

Breaking changes #

  • console, verbs: the auto-mode identifiers deprecated since v0.9.0 are removed, and the status answer no longer carries auto_approve (#120, details below). Locks to fix when they bump lockkit: lockmq (cmd/lockmq/console.go, internal/mqconsole/handlers.go), lockfile (cmd/lockfile/console.go; internal/clientcmd/sources.go reads auto_approve, harmless) and lockhttp (cmd/lockhttp/console.go). locksql is not affected.
  • ticket: ticket.Subject holds a slice (Inputs) and is no longer comparable with ==: compare with reflect.DeepEqual (#174). locksql, lockmq, lockfile, locklog, lockhttp and lockhub do not name ticket.Subject outside tests today.

Changes #

  • Breaking: the auto-mode identifiers, deprecated since v0.9.0, are removed: console.Options.Auto, ServerConfig.Auto, Env.Auto(), (*Server).Auto(), Approval.Auto, StatusView.Auto, StatusResult.AutoApprove (the auto_approve field of the status answer is gone from the wire), the Words fields AutoStart, AutoOn, Manual, NoAutoProduction, NoAutoHub, StatusAuto, StatusManual and ReviewAuto, and StatusModeRow. Screen.NeverAuto and ticket Flags.NeverAuto stay (hub auto rules). Callers to fix when each lock bumps lockkit: lockmq cmd/lockmq/console.go (Auto:) and internal/mqconsole/handlers.go (env.Auto()); lockfile cmd/lockfile/console.go (Auto:) and internal/clientcmd/sources.go (reads auto_approve: a missing field decodes as false, harmless); lockhttp cmd/lockhttp/console.go (Auto:). locksql uses its own fields (#120).
  • verbs: Parse reads the flag name of a flag package error from its fixed shapes only (a Value's Set error naming -port no longer names --port), and returns flag.ErrHelp unchanged for -h/-help so that the launcher prints its usage and exits 0. RefuseSecrets refuses a value that is itself a secret flag (--user --password X is secret, naming --user). WantsJSON takes every strconv.ParseBool form of --json= and the last occurrence wins (#120).
  • config: SecretKey also refuses passphrase, apikey, bearer and cookie, splits camelCase parts and tries adjacent parts joined (apiKey, api-key, api_key, authToken); credentials stays allowed (a contract flag). New EmbedsSecret adds a bearer token, an Authorization: header and a URL with any userinfo (https://TOKEN@host) to EmbedsPassword; verbs and CheckValue use it. EmbedsPassword no longer refuses https://mq:15672/x@y or /data/v1:2@x.db: it looks only at a URL's userinfo, and at user:pw@host without a "/" before the "@". CheckValue's error now reads "looks like it embeds a secret (a password or a token)" (#120).
  • GUIDE §14: every profile add, the interactive one too, parses with verbs.Parse; -h handling; verbs/testdata/launcher is the end-to-end example (#120).
  • agentwire: a project Init(root, "claude") no longer allows any call when .mcp.json already holds a <name> server it did not write (kept): .claude/settings.local.json is left alone and the notes warn the human to check that server instead of asking to approve it. The sentence before the allow list no longer says the calls "never touch data": the product sets it with the new Assets.ClaudeAllowNote, else a neutral default applies (#109).
  • console: on its very first start (the runtime directory not made yet) a console makes its private socket directory before the first question, so the start's questions (policy review, PRODUCTION name, User:, host key, secrets) reach the question file; one that cannot be made prints one warning and keeps them on the terminal. New ipc.PrivateDir, which Listen uses (#180).
  • console: a product with its own start-up (NewServer, not Run) gets the managed configuration's behaviour: ServerConfig.LoadManaged (exported, set to managed.Load) re-reads the file at every policy poll, and CheckManaged / ManagedLines refuse a policy beyond the bounds at start (#176).
  • pii: a public detection bench. TestBench measures precision, recall and F1 of every detector per language (en, fr, nl, de), the masked share and the field-name heuristics on a generated synthetic corpus (pii/testdata/bench, go run ./pii/internal/benchgen), and fails when a metric falls below baseline.json; the figures and known gaps are in pii/testdata/bench/RESULTS.md (#129).
  • managed: Load accepts managed.toml.verified, the root-owned stamp lox writes with VerifiedStamp once it verified the signature on the host: a match counts the file signed without cosign (a lock in a container has none), a mismatch is a refusal (#175).
  • update: Identity.PublicKey verifies a blob signed with a cosign key pair (verify-blob --key) for signers without an OIDC identity; it takes the PEM bytes of a PKIX public key, never a path, and still requires a Rekor entry (#15).
  • console: :reopen puts the loosening the human refused (at start or at :review) back to pending while it is still in the config files, so that :review can approve it later without editing the files; it is audited policy / reopened, and only the human's console reaches it (#158).
  • console: the pending policy diff shown at :review is printed through PrintContext, so it reaches the question file of the "Apply these changes?" decision: a human answering from lockhub or a phone sees the change they apply (#160). A question file whose context exceeds MaxQuestionLines or MaxQuestionBytes now starts it with … N earlier lines not shown instead of dropping its oldest lines silently.
  • ticket, console, hubclient, plans, audit: a plan's read or action ticket can ask the human for typed values, sealed end to end to the lock (#174). HubTicket.Inputs names them (Subject.Inputs); the hub client gives the ticket a one-use key (Subject.SecretTo, private half in plans.Store memory only) and Flags.NeverAuto; the decider seals the values with ticket.SealInputs (context lockkit/plan-inputs/v1) and Begin returns them in Plan.Inputs (Decision.OpenInputs; wipe them after the run). A restarted lock answers ticket.ErrExpired and renews. audit.Base.Inputs records the names, never a value. ticket.Subject holds a slice now: compare it with reflect.DeepEqual, not ==.

v0.17.0 (2026-11-13) #

  • console: Profile.TicketLimits (hubclient.Limits, set from config.Base.TicketLimits()) reaches the console's hub client on start and whenever a policy comes in force: a profile's limits.* raise applies once reviewed (typed name on production), a cut at once. A lock no longer calls Hub.Client.SetLimits itself (#150).

  • pii: LooksPersonNameColumn(table, column, samples) reports a name-like column (name, full_name, nom, naam, customer_name) whose sampled values are person names, the table name raising the bar (products, roles, cities) or lowering it (customers, users, contacts). The column name alone never decides; a lock samples the text columns LooksPersonalColumn leaves out to propose a customers.name holding "Alice Dupont" (#163).

  • managed: new package, the company-managed configuration of the whole family (#166, lockfamily#31): Load reads /etc/lockfamily/managed.toml (Linux) or /Library/Application Support/lockfamily/managed.toml (macOS), refusing a file or directory not owned by root or writable by others, an invalid file or an unknown key; an optional cosign signature (managed.toml.sigstore.json, checked against signer.toml) must verify when present, and is mandatory under [family] require_signature. File.Decode decodes a section strictly, Bounds/Check check a policy against [<tool>.bounds] through the product's own Diff, MergeProfiles makes a managed profile win and refuses its redefinition, ClampDuration clamps a duration the ClampTTL way. File.Family.RequireConfinedAgent is lockkit#133's rule. The path moves only in a lockfamily_managed_test build (SetTestPath).

  • console: the managed file bounds the policy. Run refuses an unreadable managed file or a policy beyond its bounds before any prompt (ErrManaged, audited refused-managed); CheckPolicy re-reads it at every poll, refuses a loosening beyond the bounds without a prompt (never pending; :review lists it as not approvable), applies a stricter file at once (or ends the session) and ends the session when it becomes unreadable. A backend implements ManagedBackend to be bounded; one that does not refuses a managed file with its section.

  • config: SaveApprovedManaged records the managed fingerprint (managed_sha256) in the approved policy; audit: Base.Managed carries it on every console record.

  • hubclient: Client.Await waits a bounded time for the decision on a ticket, as Watch polls it; DecisionWait (2 s) is the wait of a run.

  • console: env.Approve in hub mode waits up to DecisionWait for the decision on the ticket it pushed: a ticket an auto rule decided by then comes back Approval.Decided (with Pending), and the product runs it in the same call (Client.Begin) instead of answering pending; the agent of an AUTO-mode lox run gets its rows in one call (#162). Awaiter is the optional AsyncApprover method behind it. A product that ignores Decided answers pending as before.

  • hubclient/hubtest: Hub.AutoDecide approves every accepted ticket at once by an auto rule, as lox run's auto mode.

  • mcpkit: a tools/call result that a product's receiving middleware built (locklog's rewrite of the SDK's argument-validation errors) now carries the resultType a client at revision 2026-07-28 requires; Claude Code rejected it as malformed, so an out-of-range argument or an unknown property gave the agent an opaque client error instead of the lock's refusal (#164). Serve adds the outermost receiving middleware that sets it: add a product's own before Serve.

  • mcpkit: New accepts only the pinned ProtocolVersions() (2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05), so an SDK upgrade cannot negotiate an untested revision.

v0.16.1 (2026-11-09) #

  • hubclient: a Begin racing EndDenied (Watch) on a denied ticket no longer fails with plans: transition not allowed: denied → denied: it answers the denial with its reason (ErrDenied, or ErrWithdrawn) and the decision, for the lock's audit. Of runs racing on one denial, exactly one answers it; the others get plans.ErrUsed and no decision, so the lock audits the denial once (bug in v0.16.0).

v0.16.0 (2026-11-08) #

  • console: Asker answers a ticket refused over the hub client's limits with that ask's own *RateLimitError text: the limit, the count, the limits.* setting that raises it and the retry-after (was a general "too many requests are waiting"). A hub's own 429 keeps the general words. A lock's own wrapper for it (locksql's rateApprover) is no longer needed (#150).
  • ticket: read and read-pii tickets expire after 1 h (Risk.TTL, was never), and their approvals age out after 1 h too; ClampTTL still lets an instance shorten it. WithdrawnReason and Decision.Withdrawal name the denial a hub sends for a ticket the human withdrew there (lockhub#148).
  • hubclient: Client.EndDenied ends a pending plan as soon as its denial is polled, without waiting for the agent: plans.Withdrawn for a withdrawal at the hub, plans.Denied otherwise; the plan frees its MaxPending slot and Sync never pushes it again. Watch calls it after each poll. The agent's next Begin on the ticket answers the denial once, with the decision: ErrWithdrawn (also ErrDenied) for a withdrawal, ErrDenied with the reason otherwise.
  • plans: Store.EndTicket ends the plan of a ticket for the lock itself; Store.Pending ends Expired the plans past their Expires instead of listing them, so they no longer count against MaxPending nor go to the hub again.
  • sanitize: Terminal also escapes the other format characters (zero-width U+200B-D, U+2060, the BOM U+FEFF, soft hyphen U+00AD, U+180E, interlinear annotations U+FFF9-B, tag characters U+E0000-E007F), the other default-ignorable code points and the variation selectors (U+FE00-F, U+E0100-E01EF), as \uNNNN or \UNNNNNNNN like the bidi controls: a client cannot hide or smuggle text in what the human reads (an approval screen's note, a change request). An emoji's variation selector or joiner shows escaped too (#116).
  • console: the question file (QuestionPath) also holds the context lines the console printed above the question since the previous one (the policy diff, a host key fingerprint, the PRODUCTION banner): the prompt stays the first line, then QuestionContextMark and the lines, colours dropped and controls escaped, at most MaxQuestionLines (40) and MaxQuestionBytes (8 KiB). Context is opt-in: only lines printed with console.PrintContext (the ContextPrinter of a Terminal or a Server) are recorded, never a Println line, so a clear result or an unmasked value shown to the human never reaches the file; Run prints the PRODUCTION banner and the policy under review that way. Never what the human types. ParseQuestion reads the file, with or without context, so that lox run shows a question printed before it attached with its context (#152).

v0.15.0 (2026-11-07) #

  • hubclient: a lock opens at most 30 tickets a human decides per 5 minutes (was 10) and 30 pending ones (was 20). A ticket an auto rule of lox run decided (default:read, session:connect, ...) leaves that count once its decision is polled; a safety cap of 300 tickets per window (RateTotal, Config.RateTotal) still counts every one.
  • hubclient: a refusal is a *RateLimitError (still ErrRateLimited) naming the limit (LimitRate, LimitPending, LimitTotal), the count and the retry-after, and the setting that raises it. Limits, DefaultLimits, Limits.Effective, Client.SetLimits and Client.Limits change and read the limits at runtime.
  • config: Base gains tickets_per_window, ticket_window and max_pending (the profile's limits table); DiffBase marks more tickets, a shorter window or more pending ones a loosening. Base.TicketLimits gives the hubclient.Limits to apply (#150).

v0.14.1 (2026-11-07) #

  • Texts name lox run (lox up is deprecated): client.StartHint and client.StartCommand, console.ErrNoHub, the help of the --hub, --devices-file and --facade-keys-file flags, GUIDE.md and doc comments. No behavior, flag, label or file name changes (#147).
  • README: a library README on the Lock Family template (#146).

v0.14.0 (2026-11-06) #

  • tunnel.Tunnel.NewSession opens a session channel on the connection, for a lock that runs commands on the host it reached (lockssh) instead of forwarding through it; it fails once the tunnel is closed.

v0.13.1 (2026-11-06) #

  • A pii.add refused by the product's backend is audited by the console as policy/failed with the client's peer (peer_uid, peer_pid, peer_user), also before the agent's first other call: a CheckMask refusal with the fixed error pii.add refused by the backend: invalid pattern and no pattern (neither the raw pattern nor the backend's text, which may quote it), a SaveMask refusal with the canonical pattern and mask rule not saved: <error>. PIIBackend is unchanged; products (locklog) no longer audit these refusals themselves, or the record is written twice (#142).
  • README: the first line says lockkit is part of the Lock Family, harness security for AI agents (lockfamily#19).

v0.13.0 (2026-11-06) #

  • Ticket kinds: a ticket.VersionKinds (2) ticket names what it asks for (Kind: connect, read, write, ddl, action, endpoint, policy, each with the risk classes KindRisks allows) and its structured request (Subject: profile, endpoint, credential source, verb, target), for lox run's rules; a version-2 decision names its deciding Rule (auto rule id, or human) and may carry the backend's secret sealed to the lock (Secret; SealSecret, OpenSecret). Version-1 tickets and decisions encode and verify unchanged; a version-2 ticket needs a version-2 decision, and an auto approval of a NeverAuto ticket is ErrAuto. New fixtures ticket-connect-v2.json, decision-connect-approved-v2.json, keys-connect-v2.json. The unmasked subject is bounded: Endpoint is host[:port][/path] (no DSN keywords, query, options, escapes or whitespace), Target has no whitespace, query or fragment; Subject.Validate and NewConnectTicket refuse the rest (#132).
  • New package lazyconn: a lock opens a profile's backend connection on the approval of a connect ticket (hubclient.Client.Ask), shares it, closes it when idle (DefaultIdle, configurable) and asks again afterwards; the secret of an ask profile goes from the decider to the lock sealed, never through the agent or the hub in clear (#132).
  • console.HubTicket.Kind/Subject push a plan's ticket as a version-2 one; a renewal keeps the expired ticket's kind and subject; hubtest.Hub answers a version-2 ticket with a version-2 decision and lists what it sent (Decisions) (#132).
  • A policy change of a connection setting ends the console session, a tightening as well as an approved loosening: besides where/user/host/ port, the console now checks the credentials mode and Vault path (Profile.Credentials, Profile.VaultPath) and every shared connection field of the policy diff (config.ConnectionField: tls, tls_ca, tls_cert, tls_key, ssh and ssh.*, as config.DiffBase names them). A tightened tls mode or bastion is no longer announced as applied while the live connection keeps the old settings (#7).
  • The idle timeout counts only the human's input (console lines and control keys) and the backend's methods as activity, a method's end included (a long approval is activity): hello, status, pii.list, pii.add, change.request no longer reset it, so an agent polling status no longer keeps an unattended console open until max_session (#10).
  • mcpkit.Exact (and so ExactRows) also turns a plain int or uint outside ±2^53 into its decimal string, as it did for json.Number, int64 and uint64: such a cell no longer reaches the client rounded by the SDK's float64 output validation (#11). Products with their own copy of this rule (locksql's exactCell) should call mcpkit.Exact.
  • A client's pii.add is audited with the client behind it (peer_uid, peer_pid, peer_user) and the pattern added (new audit.Base field Pattern, JSON pattern), on the tightened record and on any failed/unsaved record of that change: it is told apart from a tightening the human made in the files, which has neither (#6).
  • On a production profile, every widening the console decides itself takes the profile name typed exactly instead of y, as a request does: the policy review at start and a loosening at :review (Apply these changes? Type the profile name "prod" to apply them: ; turning production off is a loosening of a production profile, so it takes the name too) and :unmask (Allow unmasked output? Type the profile name "prod" to allow it: ). Outside production they still take y. When the start-up review of console.Run was approved with the name, the second Type the profile name "P" to continue: prompt is no longer asked; the name record (ConfirmedKey(key)) is written as before (#8).
  • New console.WidenDecision(question, verb, forText, production, profile): the Decision of such a widening (question [y/N] outside production, the typed profile name on production), for a product's own widenings (#8).

v0.12.1 (2026-11-04) #

  • console.Run asks a production profile's name only when the policy review was approved at this start, or when the policy in force loosens the one the name was last typed for (record ConfirmedKey(key) in the approved-policy store, written after the right name), as locksql v0.16.1 does: a plain restart or recreation of an approved, unchanged production profile goes straight to its credentials (#127).
  • A console keeps the question it waits for in QuestionPath(socket), next to its socket, removed once answered (Terminal.SetQuestionFile, QuestionRecorder, RecordQuestions; Run turns it on): lox attach and lea show the pending question instead of guessing it (#127).

v0.12.0 (2026-11-02) #

  • New risk class ticket.RiskGrant (grant), between read-pii and write: one approval for a bounded read scope (database, tables, duration, read budget) that the lock then runs reads inside without a ticket each. No schema change: the scope is the plan (PlanHash is the hash of its canonical JSON, so the signed decision binds exactly that scope), Screen shows it, the lock enforces it. A grant ticket waits 4 h (TTL, ClampTTL may only shorten it) and is always Flags.NeverAuto: NewTicket sets it and Validate (so Sign and VerifyTicket) refuses a grant ticket without it; the console keeps it on the tickets it pushes. New fixtures ticket-grant-v1.json and decision-grant-approved-v1.json (#123).

v0.11.0 (2026-11-01) #

  • A client reaches one lock among several of its type, by instance: client.DialInstance(p, cwd, profile, instance) and client.SocketPathInstance dial <shared socket dir>/<instance>/... (project key, profile and peer check unchanged; instance "" is Dial/SocketPath, byte for byte); client.SocketDirInstance is the directory a product that lists its running consoles reads. client.ResolveInstance(p, flag) picks the instance from --instance NAME (client.FlagInstance), else <PREFIX>INSTANCE, else the one name lea lists in LEA_<PREFIX>INSTANCES. Several listed and none chosen: several <tool> locks: <a> <b>; pass --instance NAME (or set <PREFIX>INSTANCE); a name not listed: no <tool> lock <name>: <a> <b>. Names follow lox's instance rule (client.ErrBadInstance, whose errors name the flag or variable, never the value) and are checked before any path is built. An instance needs the separated setup. When the instance's directory does not exist (a leftover --instance or <PREFIX>INSTANCE beside a single lock), DialInstance says so and names both sources instead of the lox up hint. Each Lock wires --instance into its client commands and mcp (GUIDE.md §7).

Breaking: the console asks the Vault token at its start.

  • console.Options.VaultRead now takes the token: func(ctx context.Context, path string, token []byte) (*secrets.VaultCredentials, error) (it was func(ctx, path)); the default is secrets.VaultReadToken. A caller that sets it must take the token.
  • With credentials = "vault" the console asks Vault token for <VAULT_ADDR>: at its start, masked, and keeps the token in memory only, wiped when the console ends. It never reads VAULT_TOKEN or ~/.vault-token, so a console started by lox up can log in.
  • When Vault refuses the read the console asks again (3 tries in all) and names the path whose policy the token must be allowed to read; when Vault is unreachable it does not ask again. Reconnections reuse the token. The token is never in an argument, the audit trail, an error or the output.
  • GUIDE.md §10 and §14 say so.

v0.10.0 (2026-11-01) #

Breaking: the deprecated standalone rollback is removed.

  • console.WithdrawStandalone, console.WithdrawnResp, console.StandaloneReason and console.Withdrawn are gone (deprecated since v0.9.0, WithdrawStandalone inert since v0.9.1; no caller left in the Locks or lea). ipc.CodeWithdrawn stays, for clients of older consoles (#118, #119).
  • Comments say lox up, the host CLI's name since lockhub v0.3.0 (#118, #119).
  • New package verbs and GUIDE.md §14, the launcher verbs contract that lox init drives every lock with: <lock> init <agent>, <lock> profile add [--non-interactive] [--json] ... and <lock> doctor [--fix [--yes]] [--json]. verbs.Parse parses a non-interactive profile add without ever quoting an argument in its errors (the flag package's would), then verbs.RefuseSecrets refuses a flag named like a secret (--password, --vault-token, ...), a value that embeds a password, a control character and any positional argument, never quoting the value; verbs.WantsJSON finds --json in arguments that do not parse; verbs.Credentials accepts ask (the console asks at start) or vault with a checked --vault-path, and refuses any other source (the keychain included). Both wrap verbs.ErrSecret. verbs.Profile, verbs.Error (codes usage, secret, exists, failed) and verbs.Doctor / verbs.NewDoctor are the JSON lox init reads (#111, #117).

v0.9.1 (2026-10-31) #

  • console.Options.PolicyOnly: a console that serves its policy and decides no request, for a lock whose requests another process's hub decides (lockhttp: its gate asks lockhub; lox up starts its console without hub flags). Run starts without hub options and refuses them with it (console.ErrPolicyOnlyHub), and refuses Options.OnHub; it opens no hub. Every request handed to Env.Approve is refused (console.ErrPolicyOnly, ipc.CodeDenied, audited refused), never asked on the terminal nor approved; the console's own decisions (its policy at start and at a loosening, :unmask) are still asked on its terminal. console.PolicyOnlyApprover(base) is its approver, for a product with its own console lifecycle. Fixes lockhttp's console, which lockkit v0.9.0 refused to start under lox up (#114).
  • console.WithdrawStandalone (deprecated) is inert: it withdraws nothing and touches no file. Called by a product, it ended the pending plans of every hub instance whose console was not running (#114).
  • client.StartCommand is lox up (its arguments are no longer used), and the client.NoConsoleError hint is client.StartHint, "the console is started by lox up only: ask the user to run lox up", with no profile or project in it: <lock> console --profile … refuses to start since v0.9.0. mcpkit.NoConsole answers the same for a bare client.ErrNoConsole (#114).

v0.9.0 (2026-10-31) #

Breaking: a console is hub-attached only. There is no standalone console and no auto mode any more (#110, #112): --hub, --hub-key-file and --instance are mandatory, and --auto is refused.

  • console.Run requires its hub options: without --hub, --hub-key-file and --instance (or with only some of them) it refuses before any check, prompt or file write with console.ErrNoHub ("the console is launched by lox up only"). The standalone start, which withdrew the plans hub mode had left pending, is gone, and so is the console.Server refusal of a run by ticket without a hub (#112).
  • Auto mode is gone: Options.Auto (--auto) is refused by Run, ServerConfig.Auto by NewServer; Shift+Tab and :mode do nothing, the status row and :status have no mode. Requests are decided on lockhub (whose own auto mode honours Screen.NeverAuto); the console's own decisions (a policy change, :unmask) are still asked on its terminal, by the approver hub.Approver wraps (#112).
  • Deprecated, to be removed in the next minor release (kept so that products still using them build): Options.Auto, ServerConfig.Auto, Env.Auto (always false), Approval.Auto, StatusView.Auto, StatusResult.AutoApprove (auto_approve, always false), the auto Words (AutoStart, AutoOn, Manual, NoAutoProduction, NoAutoHub, StatusAuto, StatusManual, ReviewAuto, never printed), StatusModeRow, WithdrawStandalone, WithdrawnResp, StandaloneReason, Withdrawn. Removed: console.TicketParam (#112).
  • New: console.ErrNoHub (#112).

v0.8.1 (2026-10-31) #

Behaviour change: a hub URL with a path (https://h/lockhub), accepted before, is now refused; lockhub serves /v1/ at its root and the request signature covers the path, so such a URL never reached a working hub.

  • The hub URL is http(s)://host[:port] only: hubclient.New and the console's --hub check refuse a path (https://h// included), query, fragment, IPv6 zone, a host that is not a DNS name or an IP address (url.Parse takes ;&=!$'()*+, in a host) or a port outside 1-65535, as they refuse a user or password, without quoting the URL. hubclient.CanonicalURL returns that form, Client.URL the hub as the client prints it: the console's start line uses it, and console.HubOptions.Args passes the hub on in that form only (an invalid --instance or --require-device as a placeholder the new console refuses). A hub that cannot be reached is an ErrUnreachable naming the hub by scheme and host, no longer net/http's error quoting the whole request URL (#107).
  • More errors name the flag or key and the allowed form, never the value given (a token or URL pasted in the wrong place): ticket.ParseRisk (and --require-device), invalid instance names (--instance, hubclient.New, InstanceKey, HubKeyCommand, whose unknown flag is no longer quoted), config.CheckProfileName, the console's --profile mismatch, ipc socket names, sysconf's service_user and client_group lookups, and the console's wrong-account and console-is-a-client refusals (they name service_user, client_group) (#107).
  • New: ticket.RiskNames (#107).

v0.8.0 (2026-10-31) #

  • pii: FindInViews, Find and DetectionView(s): detected values as ranges with their kind, also behind percent, JSON, HTML, \xHH, \e, \033 and form escapes (alone or chained), look-alikes, invisible characters, control sequences (also escaped), invalid UTF-8 bytes and values cut by the scan limit (#93, #94). New dependency: golang.org/x/text v0.43.0 (unicode/norm); golang.org/x/sync moves to v0.24.0.
  • pii: two opt-in detectors, ip (public IPv4 and IPv6 addresses, IPv4-mapped) and secret (credentials, tokens, private keys, masked as <redacted> and run first), and pii.LooksSecret(name). Neither is in AllDetectors: a product masks exactly what it masked before until it names them (GUIDE §8, #96, #97).
  • pii: every detector reads a \n, \r or \t escape, and the \u0026-style escapes of &, <, >, = and quotes (JSON written by zap, logrus, bunyan or Go's encoding/json), as the blank or delimiter it stands for, so a value right after one is found: {"msg":"tel:\t0475123456"} is now masked. This is the only output change for products that keep AllDetectors (#97).
  • pii: the secret detector checks for a literal its token, scheme, userinfo and command-line regexps need before running them, and starts its key=value search near the next separator: about 7 MB/s per core on plain log lines instead of 1 MB/s (BenchmarkSecretBenignCorpus), same findings. CI fails under 2.5 MB/s (TestSecretThroughputFloor). A value opened by a \u0022 escape is masked after the escape, not from inside it, so the output stays valid JSON (#105).
  • pii: keyed pseudonyms: Pseudonymizer.ID(kind, value) (HMAC-SHA256, per-epoch key, 12 Crockford base32 characters), Normalise (one ID per spelling of a value), NewEpoch and OpenEpochStore (private 0600/0700 key files, one process per directory, Rotate, RotationDue, Prune) (GUIDE §8, #95, #98).
  • pii: opt-in person_name detector (gazetteer of given and family names, titles), not in AllDetectors (GUIDE §8). Recall 98.9 % on the name corpus and 10 % on held-out names outside the gazetteer; false positives 0 % of 2 000 Loghub-style lines and 2.4 % of held-out ticket and prose lines (#101).
  • agentwire: Assets.ClaudeAllowProject merges ClaudeAllow into the project's .claude/settings.local.json on a project Init (off by default; GUIDE §12, #103).
  • agentwire: merged permissions.allow entries (user and project scope) and the notes keep &, < and > as written instead of \u0026-style escapes (#103).

v0.7.2 (2026-10-31) #

  • Errors no longer echo a value that may be a secret pasted in the wrong key (locklog LL-02): config.ParseSSH names ssh.credentials and the allowed modes, pii.Detectors the entry's position and the known detectors, sysconf.Load the finger key, and hubclient.New the hub by scheme and host only (#99).
  • hubclient.New refuses a hub URL that carries a user or password: it reached net/http's errors, the console's start line and the hub as a Basic header (the hub authenticates by signature) (#99).

v0.7.1 (2026-10-30) #

  • A policy change that ends the session when applied (a tightening read from the files: production refused, connection settings changed, the backend refusing it; a loosening at :review whose policy in force could not be restored) no longer prints "policy tightened:" or "policy change not applied" after the line that ends the session (#89).

v0.7.0 (2026-10-30) #

Console product hooks (locksql#96: locksql's console runs on this one). A backend that implements none of them keeps the console's behaviour, so lockfile and lockmq see no change; the console goldens are unchanged.

  • Backend.Connect gets the policy in force: Credentials.Policy (#12), at start and at every reconnection (ServerConfig.ReconnectPolicy, used instead of Reconnect when set; Run sets it).
  • console.Unwrap: the backend's own session behind a Vault-leased one (expiringSession.Unwrap) (#13).
  • Optional backend interfaces: StatusAnswerer (the whole status answer, flat), StatusRower and StatusLiner (:status rows, status row parts; StatusView, StatusModeRow, StatusResultsRow, StatusUnmaskRow, StatusPendingRow, StatusBoundaryRow), ChangeFormatter (policy diffs at start, on a tightening, at :review), ConnectionComparer (more changes of the policy that end the session, on top of where/user/host/port), PolicyApplier (prepares the backend for a policy before it is in force; a refused loosening stays out, a refused tightening is audited and ends the session, a failed save applies the policy in force back, and ends the session if that fails; NewServer refuses a start policy it refuses, audited by Run), Worder (Words: the console's lines in the product's terms; an empty field keeps the console's, NoAutoHub refuses auto mode in hub mode).
  • EndError: a Reconnect error that ends the session with its reason.
  • AskerOf(env) and PolicyPendingOf(env) for a handler that paints its own approval screen; Server.Asker, Server.WriteAudit (returns the write failure, counted like the console's own), Server.PolicyRefused.
  • A tightening that cannot be saved as the approved policy now applies anyway (audited policy/unsaved, told on the console): before, the looser policy stayed in force (fail open). A loosening that cannot be saved still stays out.
  • :review no longer says "policy change applied" when applying it ended the session (connection settings changed, production refused): the console already said why.

v0.6.0 (2026-10-30) #

Renewal hardening, from wiring v0.5.0 into the locks: an input a lock cannot check now is named unchecked (never one click); a chain of renewals diffs against the human's last approval; an aged approval can be renewed from the hub before the agent's run; console.Options.OnHub registers the recompute before any request. Hubs and facades must run v0.6.0 before locks send unchecked.

  • ticket.UncheckedInput / ChangeUnchecked: a Recompute marks an input it cannot check now (no session) instead of passing its stored digest; the renewal names it unchecked and is never one click.
  • hubclient: renewing a ticket whose renewal expired renews that renewal (one link at a time); one whose renewal ran, was denied or withdrawn is not_renewable (it no longer returns a used ticket). A renewal in a chain lists the changes since what the human last approved (the newest approved link, else the first plan), never since an undecided link (plans.Store.PlanByID); a pruned link makes it unchecked. A renewal denied at the hub, or whose approval aged, is handled before the agent's run. Facades and hubs must run lockkit with unchecked (this release) before locks send it (#84).
  • hubclient: a hub renew request for a pending ticket whose approval aged past its TTL ends the plan Expired (ExpireApproval, as Begin would at the agent's run) and renews it: the hub can offer an aged approval for re-approval before the agent comes back (#81).
  • console.Options.OnHub: called with the hub once Run opened it, before any request: where a product registers SetRecompute (#82).

v0.5.0 (2026-10-30) #

Reveal and re-approval (lockhub's local facades): a human at the host's terminal or the lea deck can see a ticket's clear screen, sealed by the lock to the facade's own key (the hub relays ciphertext only), and approve an expired ticket again in one step, seeing which plan-hash inputs moved. Locks wire SetRecompute, Binding.Inputs and Hub.RenewResp; lock up pins the facade keys (--facade-keys-file). #78 merged after the v0.4.0 tag; nothing else changed.

  • Reveal (Ctrl-O on a local facade): ticket.HubRequest (signed by the hub, ContextHubRequest, kinds reveal and renew) arrives in the requests of the decisions long poll; ticket.RevealKey is the facade's ephemeral X25519 key signed by its Ed25519 key (ContextRevealKey, NewRevealKey, Verify against the lock's PinnedFacades; CanReveal: tty, deck only); the lock answers POST /v1/requests/{id}/answer with a RevealAnswer signed by its key (SealReveal, ContextRevealAnswer), which the facade opens with OpenReveal. The hub relays ciphertext only. FacadeSet, ParseFacades, hubclient.ReadFacades, Config.Facades, console flag --facade-keys-file (HubOptions.FacadeKeysFile, in Args). SubmitClear keeps the clear screen in the plan (plans.Plan.Clear, MarkPendingClear), the one both reveals and devices see.
  • Re-approval of expired tickets (H1): Ticket.Renews (Renewal: the expired ticket, expired or approval-expired with approved_at, checked, changes: the names of the plan-hash inputs that moved (changed/new/gone), never values nor digests; InputDigests stays in the lock, DiffInputs, Unchanged). A changed policy fingerprint is no renewal (policy_changed: plan again). hubclient.Client.Renew (agent path) and the hub's renew request make a new ticket for the same plan, re-checked through SetRecompute; Begin answers an expired ticket ticket.ErrExpired and records an aged approval (plans.Store.ExpireApproval). plans: Binding.Inputs, Renew, ExpiredByTicket, TicketPlan, RenewalOf, OwnerOf, ErrRenewed. Failed requests are answered with a fixed code (hubclient.AnswerCode), never the error text. Console: Hub.RenewResp, Hub.Requests (audit events reveal, renew). hubtest: Request, RawRequest, Requeue, Answer.

v0.4.0 (2026-10-30) #

Approval devices (lockhub's mobile facade): a decision taken on a phone is signed by the phone itself, over the lock's own signed ticket, and the lock checks that signature: the hub's key alone cannot make an approval pass for one taken on the device, nor get one for a ticket the lock did not sign. Limits: the device runs a web app the hub serves, so a hub that serves malicious code can still ask the human to verify themselves for another pending ticket of the same lock, and read the clear screens sealed to the device while they are shown (hence sealing is a separate opt-in).

  • ticket: Device (a WebAuthn credential and an X25519 sealing key, pinned by the lock; DeviceSet, ParseDevices in canonical JSON, MarshalDevices, Fingerprint); Decision.Device, an optional WebAuthn assertion over Decision.Statement (every field a lock acts on, the approver's display name and TicketHash of the signed ticket, under ContextDeviceDecision), present only with facade mobile; VerifyAssertion (ES256, user present and verified, origin, rp id) and VerifyDevice(dev, decision, signedTicket). The hub relays the lock's ticket bytes to the device unchanged: the device hashes exactly what the lock signed. A decision without a device member is unchanged on the wire; locks on older lockkit refuse one with it (unknown member).

  • ticket: SealGCM (SealAlgGCM: X25519, HKDF-SHA256, AES-256-GCM), a sealed box WebCrypto opens; Open reads both algorithms. PII.SealedFor carries the clear screen sealed to each pinned device (SealClearScreen, up to MaxClearScreen; OpenClearScreen), omitted otherwise. Fixed vectors in testdata/device-v1.json.

  • hubclient: Config.Devices: a decision with a device signature, or from the mobile facade, runs only if it verifies under a pinned device over the lock's own ticket (checked in Begin; the decision is dropped otherwise). Config.RequireDevice: approvals of those risk classes without a device signature end the plan Denied. Config.SealClear with SubmitClear seals the clear screen to the devices. ReadDevices, ParseRisks.

  • console hub mode: --devices-file, --require-device, --seal-clear. HubOptions.Args renders the hub flags back, for products that start their console again.

  • ticket/devicetest: a virtual WebAuthn authenticator for tests.

  • Hub mode rollback: a console started without --hub withdraws the plans its hub mode left pending (console.WithdrawStandalone) and answers a run of a hub ticket with the new ipc.CodeWithdrawn (console.WithdrawnResp, console.TicketParam) before the product sees it. plans.Store.WithdrawAll marks the plans Untold (kept past the retention until told); hubclient.Client.Sync withdraws them at the hub on the next start in hub mode (Untold, MarkTold). hubtest.Hub.SetWithdrawStatus for tests.

v0.3.1 (2026-10-30) #

Fixes to hub mode found after v0.3.0. A lock reaching its hub over plain http on the hub's own network must now pass --hub-plain-http.

  • mcpkit: RequesterFromEnv sends the hash of LEA_AGENT_TOKEN (plans.HashToken), not the token: the raw token stays in the agent's process. The console's binding compares the same (#61).

  • hubclient (#57): an http:// hub URL is refused unless the host is a loopback address, or Config.PlainHTTP (console flag --hub-plain-http, HubOptions.PlainHTTP) is set for a service name without a dot or a private address; redirects are refused (a 3xx is an error, the Lock-* headers are never sent elsewhere); the state file must be a plain file with no group or other access.

  • plans: a test that the store's directory lock dies with a killed holder (#73).

v0.3.0 (2026-10-30) #

Hub support: the packages a lock needs to ask the human through lockhub instead of its own console window (ticket, render masked screens, plans, the asynchronous console and its hub mode, hubclient, the audit chain, the mcpkit pending answer). Standalone consoles keep their prompts, with stricter rules: audit writes and widenings fail closed, no widening while a prompt waits, and production refusals re-applied when the policy becomes production. Audit lines gain prev_hash at the end.

  • hubclient: the hub key file and the instance key are read through one descriptor opened without waiting, checked regular and small: a FIFO, a device or a directory is refused at once instead of blocking the console. pii.AllDetectors panics rather than return none.

  • plans (#60): Open locks the store directory (flock, LockFileEx), without waiting: a second store on the same directory (two consoles on one volume) fails with ErrLocked, so a decision nonce is accepted once. Store.Close releases it; console.Hub.Close too, and Run closes its hub when it returns.

  • audit (#59): VerifyAnchored(r, Anchor) checks a log against an anchor kept elsewhere (the hub's): a log cut before it, or whose chain was stripped (every prev_hash removed, which Verify alone reads as legacy), is a *ChainError. Summary.Anchor() makes the anchor.

  • render (#58): MaskScreen with no detectors runs every detector (pii.AllDetectors: all but the opt-in phone_loose), never none: a product that forgets pii.Detectors sends no header, note or structure in clear.

  • console (#63): a product handler that panics no longer takes the console down: its client gets an internal error with a generic text, the console prints one line, the audit records a failed run, and the session goes on (and stops when its context ends). Loop recovers a panicking Handle too.

  • mcpkit, console: the pending answer names the lock instance in hub mode ("instance", the console's --instance): mcpkit.PendingOn, PendingResult.Instance, console.Pending.Instance; without one the answer is unchanged.

  • console hub mode (#28): HubOptions (--hub, --hub-key-file, --instance, all three or none, --auto refused with them) put Run in hub mode: requests with a Screen.Hub (HubTicket: plan, caller, risk, render.MaskedScreen) are pushed to lockhub and answered pending; one without is refused, never asked on the terminal. HubOf(env) gives the product the plans and the client to run a decided ticket. hubclient.InstanceKey / HubKeyCommand (<lock> hub-key --instance NAME) make and print the instance key; hubclient/hubtest is a fake hub for tests.

  • audit: prev_hash is now the last member of a line, not the second: the chain is additive at the end, so a product's pinned record format (locksql's 0.8 lines) stays a byte-identical prefix. A record naming prev_hash itself is refused. On Windows the append lock covers one byte far past the log (LockFileEx locks are mandatory), so a reader is never refused; CI runs the audit tests on Windows.

  • console (#28, part 1): Decision.Screen carries the approval screen to the approver (a terminal approver ignores it). An AsyncApprover (lockhub) takes requests with Submit: the request is answered pending (PendingResp: {"status":"pending","ticket":…,"lock":…}, a result, not an error), set as Approval.Pending and as Approval.Refusal so that a product returning the refusal answers pending. A hub that refuses or cannot be reached keeps the request pending, never asked on the terminal instead; no ticket at all fails the request. The console's auto mode is off with an AsyncApprover.

  • hubclient (new, #29): a lock's client for lockhub. Requests signed with the instance key (Lock-Instance, Lock-Time, Lock-Nonce, Lock-Sig), decisions verified under a pinned hub key only (New refuses a config without one; no discovery). Submit takes a render.MaskedScreen (an unmasked one is refused), signs the ticket, records it in the plan store and pushes it; it is idempotent (same plan, same ticket) and dedupes by plan hash, with a pending cap (20) and a rate limit (10 new tickets / 5 min). A hub that cannot be reached leaves the plan pending (ErrUnreachable), never a fallback; Sync resends. Poll/Watch long-poll decisions with a cursor kept in a 0600 state file with the decisions not yet acted on; Begin checks Match, Usable and the plan's binding, then consumes the decision nonce and returns the verified decision (approver, facade, signature for the audit); Finish and Withdraw tell the hub.

  • audit: EventPending, the record of a request handed to the hub (ticket set), written before the requester is told pending; a failed write refuses the request and withdraws its ticket (GUIDE §5).

  • console (#5): audit writes fail closed. An approval (typed or auto) whose record cannot be written, the log being unwritable or Screen.Record returning no valid record, is refused with ipc.CodeInternal, and the human sees why; three failed writes in a row end the session. Denials stay denials.

  • console: widenings take effect only once recorded. Auto mode on, clear results shown, unmask allowed and a loosened policy approved (at :review or at start) write their audit record first; if it cannot be written they stay off (the policy is neither saved nor applied; at start the console does not start), and the console says so.

  • mcpkit (#31): Pending(lock, ticket) is the shared answer of a run waiting on a hub ticket: not an error, a fixed "do not retry, end your turn" text and the structured PendingResult {"status":"pending","ticket":…,"lock":…}. RequesterFromEnv reads lea's LEA_AGENT and LEA_AGENT_TOKEN into the new ipc.Requester, carried in plan and run params; ipc.CodeWrongRequester refuses a plan or ticket to another agent's token.

  • audit (#30): hash chain and hub fields, additive. Every record now carries prev_hash, the LineHash of the line before it, Genesis on the first line of an empty log; Verify checks a log and returns a *ChainError at the first broken line (a record edited, removed, inserted or reordered). Old logs stay readable: their records count as legacy, and the first chained record links to the last of them. Base gains ticket, approver, facade and hub_sig, written in hub mode only. A cut last line is left alone and the next record starts a line of its own. Every append holds a lock on the file (flock, LockFileEx), so that two processes writing one log (a console and a product command beside it) keep one chain.

  • kit, console, ipc: the same-user refusal, the client-group and socket-directory errors no longer tell the human to run sudo <name> install, which the products' images refuse: they point to the launcher (Product.LauncherHint).

  • plans (new, #27): a lock's persisted plan store, one 0600 file per plan in a 0700 directory (wider modes and symlinks refused). A plan is bound to the peer UID, the requesting agent's token (stored hashed), the policy fingerprint and the plan hash; Check marks it stale when either hash moved, Begin runs it once under a decision nonce recorded durably (ErrReplay, ErrUsed). FindPending dedupes a repeated run onto the existing ticket. Plans survive restarts; one executing at the stop comes back failed.

  • render (#26): MaskedScreen, the only form in which an approval screen leaves the lock, built only by MaskScreen(ScreenSource, Masking): values in personal columns (pii.LooksPersonalColumn or Masking.Personal) masked whole, the detectors over everything else, the structure of the request kept. Screen() is the ticket.Screen, Touched() the kinds masked; the zero value is not Valid and does not encode (ErrUnmasked).

  • ticket: Decision.Usable refuses a decision that is not an approval (ErrDenied).

  • ticket (new, #25): the contract between a lock and lockhub. Signed Ticket (lock key) and Decision (hub key, carrying the ticket's instance, lock key id and plan hash: Match refuses a decision for another instance or plan), closed Risk classes with their TTL (reads none, write and policy-loosen 24 h, prod-write and unlock 4 h; ClampTTL only shortens), RFC 8785 canonical JSON (Canonical, CanonicalizeJSON), Ed25519 signatures with domain separation (SignBytes/VerifyBytes and Context), strict verification, X25519 sealed boxes (Seal/Open) and signed X25519 keys (KeyCert), ULID ids, nonces, HashOf, RequestMessage (what Lock-Sig signs). Only the exact canonical form verifies; Canonical refuses integers a double cannot hold and invalid UTF-8 rather than encode them with a loss. Signed fixtures under ticket/testdata for lockhub's compatibility tests.

  • console: Asker takes a console's human decisions: Ask closes an approval screen with the prompt (typed profile name on production, retype answer, fingerprint) and returns the Approval, its outcome recorded through a callback; Decide asks for any other decision. Server.Approve runs on it, unchanged, and a product that draws its own approval screen (prompting inside it) uses it too, so every approval is decided in one place.

  • console (#2): while a prompt waits, a control key or command never widens a switch (auto mode on, clear results shown, unmasked output allowed): it is refused with a line saying to answer the prompt first; narrowing stays allowed. Decision.For names the decision, and the level 2 finger prompt says it ("… on the reader to approve the request above").

  • console (#3): a policy that becomes production while the console runs re-applies every start-up refusal of a production profile before the backend's checks, and ends the session when one fails. ServerConfig.RefuseProduction carries them; Run sets it.

v0.2.1 (2026-10-28) #

  • pii (locksql#18): LooksPersonalColumn also reports mail, e_mail, login (unless the column is a date or time, as Django's last_login), username, nickname, displayname, pseudo and handle; username, nickname and displayname count inside unsplit compounds like firstname. LooksQuasi also reports free-text columns (notes, comment, bio, remarks and their French, Dutch, German and Spanish forms) and coordinates (lat, lng, lon, latitude, longitude, coordinates). neutralLast still excludes *_id, *_count, *_verified.

v0.2.0 (2026-10-28) #

  • secrets (locksql#51): VaultReadToken(ctx, path, token) reads a login from Vault with a token the caller holds, such as one the human typed: passed to the vault CLI as VAULT_TOKEN in the child's environment only (never argv, never disk), over any VAULT_TOKEN of the console and its ~/.vault-token. An empty token or one Vault refuses (expired, revoked, not allowed) is ErrVaultAuth, whose hint asks for another token rather than vault login; the token is cut out of the CLI's message. VaultRead is unchanged.

  • pii (locksql#17): Unicode e-mail domains; U+00A0, U+202F, U+2009 and full-width digits in phone, IBAN, card and national-id values; opt-in phone_loose detector for national numbers without a trunk 0; Detect scans integral floats; CutAt also retreats over Unicode letters and typographic spaces. Detectors only mask more.

  • console: clear results are refused at desktop security level 1 too (the agent's account can read the console window): Run returns ErrConfig for ShowResults at level 1, NewServer refuses ShowResults at any level but 0, and Ctrl-O / :results leave the results masked at level 1 with results stay masked at level 1: the agent's account can read this window; a refused switch is not audited. --auto and --allow-unmask are unchanged at level 1. Levels 0 and 2 are unchanged.

v0.1.0 (unreleased) #

First release: the product-neutral half of locksql, extracted with no behaviour change for locksql, for the lock* tools to share.

  • kit: kit.Product, a product's names and directories.
  • refusal: refusal codes registry (Register, Retire), refusal.Error, structured Info; E_POLICY built in.
  • secrets: terminal prompt, OS keychain, Vault CLI, Sanitize, Wipe.
  • fingerprint: fprintd verification for the desktop security level 2.
  • sysconf: the machine-wide /etc/<name>/system.toml and process, tty and relay probes.
  • ui: palette, banner, status marks.
  • audit: append-only JSONL audit log, audit.Record, audit.Base.
  • config: config lookup, secret-safe TOML decoding, profile base, TLS/SSH, Policy with the "AI tightens, human loosens" diff helpers, approved store and fingerprints.
  • ipc, client, tunnel: console sockets and peer credentials, newline-delimited JSON-RPC 2.0, the secret-free client with the "no console" hint, the SSH bastion tunnel.
  • pii: value detectors, mask modes, name heuristics, redacted references.
  • render, sanitize, update: capped typed output, error scrubbing and terminal-safe text, cosign signature checks.
  • console: terminal, approvers (typed, desktop, fingerprint), isolation checks, request loop, and the generic console framework (Run, Server, the Backend seam, PIIBackend for pii.list/pii.add).
  • mcpkit: MCP server plumbing (stdio, profiles, progress, untrusted framing, no-console answer, structured refusals, exact integers).
  • agentwire: wiring Claude Code, Codex, Cursor and Gemini CLI to a product's MCP server, in a project or for the account.