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:
LooksPersonNameColumnreads German plurals in-er(mitglieder) andstaedte/städteas table hints. The detection bench scores it on sampled name columns (namecolumns.jsonl:name/full_name/nom/naamwith EN/FR/NL/DE names, andproducts/roles/cities/tagsnegatives), 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 throughauth(--auth,--auth-local,--auth-host) keeps an auth method name (peer,scram-sha-256,md5…): initdb's--auth-local and --auth-hosthint and--auth-local peerstay readable;--auth admin:pw,--auth-token …,--auth s3cr3tT0kenand--password hunter2stay masked (#198).
v0.19.1 (2026-11-13) #
Changes #
- pii: the
email,iban,card,be_niss,fr_nir,nl_bsn,us_ssnandipfinders 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);FindInViewswithAllDetectorson 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_nirnow runs beforecard: a French NIR whose digits pass Luhn is masked as a NIR, no longer as a card number. - pii:
phonewithoutphone_loosealso finds a national number after a phone word (tel 612 345 678), and 12-digit German mobiles (0151 23456789);be_nissfinds a valid number with other separators afterNISS/rijksregister; an e-mail local part may hold an apostrophe (o'hara). - pii:
Matchhas a newConfidencefield; masked output is unchanged. - managed:
File.Decodeinto amap[string]any(or anany) now takes nested tables where it failed withunknown key(#192). Struct targets stay strict.
Changes #
- managed:
File.Decodeinto amap[string]any(or anany, 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 withunknown 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) andpostal_address(BE, FR, NL and English street addresses with their postcode and locality), both pattern only (medium). Gaps the bench found:fr_nirnow runs beforecard, so a NIR whose digits pass Luhn is a NIR (fr recall 0.783 to 1.000;AllDetectorskeeps the same detectors in that new order);phonefinds 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-onlypassport(Belgian passport numbers after a passport word) andbirth_date(a date after a birth word or in a birth field, never a date alone), both low confidence.Normalisegives a birth date asYYYY-MM-DDwhatever its spelling (#130). - pii: new opt-in detectors
eu_vat(BE, FR, NL, DE, LU VAT numbers, checksums verified) andbe_company(Belgian KBO/BCE company numbers), run afteribanand beforecard;Normalisekeeps their ASCII letters and digits, upper case.Matchhas aConfidence(ConfidenceHighfor a verified checksum,ConfidenceMediumfor a pattern,ConfidenceLowfor a gazetteer or context match); masked output is unchanged. Context words raise weak candidates to low confidence matches, additively:phone(withoutphone_loose) now also findstel 612 345 678,be_nissa valid number with other separators afterNISS/rijksregister,be_companyten bare digits afterKBO/BCE. NewFindField(field, s, ds)uses the field name as that context.AllDetectorsis 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.goreadsauto_approve, harmless) and lockhttp (cmd/lockhttp/console.go). locksql is not affected. - ticket:
ticket.Subjectholds a slice (Inputs) and is no longer comparable with==: compare withreflect.DeepEqual(#174). locksql, lockmq, lockfile, locklog, lockhttp and lockhub do not nameticket.Subjectoutside 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(theauto_approvefield of the status answer is gone from the wire), theWordsfieldsAutoStart,AutoOn,Manual,NoAutoProduction,NoAutoHub,StatusAuto,StatusManualandReviewAuto, andStatusModeRow.Screen.NeverAutoand ticketFlags.NeverAutostay (hub auto rules). Callers to fix when each lock bumps lockkit: lockmqcmd/lockmq/console.go(Auto:) andinternal/mqconsole/handlers.go(env.Auto()); lockfilecmd/lockfile/console.go(Auto:) andinternal/clientcmd/sources.go(readsauto_approve: a missing field decodes as false, harmless); lockhttpcmd/lockhttp/console.go(Auto:). locksql uses its own fields (#120). - verbs:
Parsereads the flag name of a flag package error from its fixed shapes only (aValue'sSeterror naming-portno longer names--port), and returnsflag.ErrHelpunchanged for-h/-helpso that the launcher prints its usage and exits 0.RefuseSecretsrefuses a value that is itself a secret flag (--user --password Xissecret, naming--user).WantsJSONtakes everystrconv.ParseBoolform of--json=and the last occurrence wins (#120). - config:
SecretKeyalso refusespassphrase,apikey,bearerandcookie, splits camelCase parts and tries adjacent parts joined (apiKey,api-key,api_key,authToken);credentialsstays allowed (a contract flag). NewEmbedsSecretadds a bearer token, anAuthorization:header and a URL with any userinfo (https://TOKEN@host) toEmbedsPassword;verbsandCheckValueuse it.EmbedsPasswordno longer refuseshttps://mq:15672/x@yor/data/v1:2@x.db: it looks only at a URL's userinfo, and atuser:pw@hostwithout 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 withverbs.Parse;-hhandling;verbs/testdata/launcheris the end-to-end example (#120). - agentwire: a project
Init(root, "claude")no longer allows any call when.mcp.jsonalready holds a<name>server it did not write (kept):.claude/settings.local.jsonis 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 newAssets.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. Newipc.PrivateDir, whichListenuses (#180). - console: a product with its own start-up (
NewServer, notRun) gets the managed configuration's behaviour:ServerConfig.LoadManaged(exported, set tomanaged.Load) re-reads the file at every policy poll, andCheckManaged/ManagedLinesrefuse a policy beyond the bounds at start (#176). - pii: a public detection bench.
TestBenchmeasures 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 belowbaseline.json; the figures and known gaps are inpii/testdata/bench/RESULTS.md(#129). - managed:
Loadacceptsmanaged.toml.verified, the root-owned stamp lox writes withVerifiedStamponce 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.PublicKeyverifies 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:
:reopenputs the loosening the human refused (at start or at:review) back to pending while it is still in the config files, so that:reviewcan approve it later without editing the files; it is auditedpolicy/reopened, and only the human's console reaches it (#158). - console: the pending policy diff shown at
:reviewis printed throughPrintContext, 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 exceedsMaxQuestionLinesorMaxQuestionBytesnow starts it with… N earlier lines not showninstead 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.Inputsnames them (Subject.Inputs); the hub client gives the ticket a one-use key (Subject.SecretTo, private half inplans.Storememory only) andFlags.NeverAuto; the decider seals the values withticket.SealInputs(contextlockkit/plan-inputs/v1) andBeginreturns them inPlan.Inputs(Decision.OpenInputs; wipe them after the run). A restarted lock answersticket.ErrExpiredand renews.audit.Base.Inputsrecords the names, never a value.ticket.Subjectholds a slice now: compare it withreflect.DeepEqual, not==.
v0.17.0 (2026-11-13) #
-
console:
Profile.TicketLimits(hubclient.Limits, set fromconfig.Base.TicketLimits()) reaches the console's hub client on start and whenever a policy comes in force: a profile'slimits.*raise applies once reviewed (typed name on production), a cut at once. A lock no longer callsHub.Client.SetLimitsitself (#150). -
pii:
LooksPersonNameColumn(table, column, samples)reports aname-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 columnsLooksPersonalColumnleaves out to propose acustomers.nameholding "Alice Dupont" (#163). -
managed: new package, the company-managed configuration of the whole family (#166, lockfamily#31):
Loadreads/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 againstsigner.toml) must verify when present, and is mandatory under[family] require_signature.File.Decodedecodes a section strictly,Bounds/Checkcheck a policy against[<tool>.bounds]through the product's ownDiff,MergeProfilesmakes a managed profile win and refuses its redefinition,ClampDurationclamps a duration theClampTTLway.File.Family.RequireConfinedAgentis lockkit#133's rule. The path moves only in alockfamily_managed_testbuild (SetTestPath). -
console: the managed file bounds the policy.
Runrefuses an unreadable managed file or a policy beyond its bounds before any prompt (ErrManaged, auditedrefused-managed);CheckPolicyre-reads it at every poll, refuses a loosening beyond the bounds without a prompt (never pending;:reviewlists it as not approvable), applies a stricter file at once (or ends the session) and ends the session when it becomes unreadable. A backend implementsManagedBackendto be bounded; one that does not refuses a managed file with its section. -
config:
SaveApprovedManagedrecords the managed fingerprint (managed_sha256) in the approved policy; audit:Base.Managedcarries it on every console record. -
hubclient:
Client.Awaitwaits a bounded time for the decision on a ticket, asWatchpolls it;DecisionWait(2 s) is the wait of a run. -
console:
env.Approvein hub mode waits up toDecisionWaitfor the decision on the ticket it pushed: a ticket an auto rule decided by then comes backApproval.Decided(withPending), and the product runs it in the same call (Client.Begin) instead of answering pending; the agent of an AUTO-modelox rungets its rows in one call (#162).Awaiteris the optionalAsyncApprovermethod behind it. A product that ignoresDecidedanswers pending as before. -
hubclient/hubtest:
Hub.AutoDecideapproves every accepted ticket at once by an auto rule, aslox run's auto mode. -
mcpkit: a
tools/callresult that a product's receiving middleware built (locklog's rewrite of the SDK's argument-validation errors) now carries theresultTypea 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).Serveadds the outermost receiving middleware that sets it: add a product's own beforeServe. -
mcpkit:
Newaccepts only the pinnedProtocolVersions()(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
BeginracingEndDenied(Watch) on a denied ticket no longer fails withplans: transition not allowed: denied → denied: it answers the denial with its reason (ErrDenied, orErrWithdrawn) and the decision, for the lock's audit. Of runs racing on one denial, exactly one answers it; the others getplans.ErrUsedand no decision, so the lock audits the denial once (bug in v0.16.0).
v0.16.0 (2026-11-08) #
- console:
Askeranswers a ticket refused over the hub client's limits with that ask's own*RateLimitErrortext: the limit, the count, thelimits.*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'srateApprover) is no longer needed (#150). - ticket:
readandread-piitickets expire after 1 h (Risk.TTL, was never), and their approvals age out after 1 h too;ClampTTLstill lets an instance shorten it.WithdrawnReasonandDecision.Withdrawalname the denial a hub sends for a ticket the human withdrew there (lockhub#148). - hubclient:
Client.EndDeniedends a pending plan as soon as its denial is polled, without waiting for the agent:plans.Withdrawnfor a withdrawal at the hub,plans.Deniedotherwise; the plan frees itsMaxPendingslot andSyncnever pushes it again.Watchcalls it after each poll. The agent's nextBeginon the ticket answers the denial once, with the decision:ErrWithdrawn(alsoErrDenied) for a withdrawal,ErrDeniedwith the reason otherwise. - plans:
Store.EndTicketends the plan of a ticket for the lock itself;Store.Pendingends Expired the plans past theirExpiresinstead of listing them, so they no longer count againstMaxPendingnor go to the hub again. - sanitize:
Terminalalso 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\uNNNNor\UNNNNNNNNlike 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, thenQuestionContextMarkand the lines, colours dropped and controls escaped, at mostMaxQuestionLines(40) andMaxQuestionBytes(8 KiB). Context is opt-in: only lines printed withconsole.PrintContext(theContextPrinterof aTerminalor aServer) are recorded, never aPrintlnline, so a clear result or an unmasked value shown to the human never reaches the file;Runprints the PRODUCTION banner and the policy under review that way. Never what the human types.ParseQuestionreads the file, with or without context, so thatlox runshows 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 rundecided (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(stillErrRateLimited) naming the limit (LimitRate,LimitPending,LimitTotal), the count and the retry-after, and the setting that raises it.Limits,DefaultLimits,Limits.Effective,Client.SetLimitsandClient.Limitschange and read the limits at runtime. - config:
Basegainstickets_per_window,ticket_windowandmax_pending(the profile'slimitstable);DiffBasemarks more tickets, a shorter window or more pending ones a loosening.Base.TicketLimitsgives thehubclient.Limitsto apply (#150).
v0.14.1 (2026-11-07) #
- Texts name
lox run(lox upis deprecated):client.StartHintandclient.StartCommand,console.ErrNoHub, the help of the--hub,--devices-fileand--facade-keys-fileflags, 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.NewSessionopens 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.addrefused by the product's backend is audited by the console aspolicy/failedwith the client's peer (peer_uid,peer_pid,peer_user), also before the agent's first other call: aCheckMaskrefusal with the fixed errorpii.add refused by the backend: invalid patternand no pattern (neither the raw pattern nor the backend's text, which may quote it), aSaveMaskrefusal with the canonicalpatternandmask rule not saved: <error>.PIIBackendis 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 classesKindRisksallows) and its structured request (Subject: profile, endpoint, credential source, verb, target), forlox run's rules; a version-2 decision names its decidingRule(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 aNeverAutoticket isErrAuto. New fixturesticket-connect-v2.json,decision-connect-approved-v2.json,keys-connect-v2.json. The unmasked subject is bounded:Endpointishost[:port][/path](no DSN keywords, query, options, escapes or whitespace),Targethas no whitespace, query or fragment;Subject.ValidateandNewConnectTicketrefuse 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 anaskprofile goes from the decider to the lock sealed, never through the agent or the hub in clear (#132). console.HubTicket.Kind/Subjectpush a plan's ticket as a version-2 one; a renewal keeps the expired ticket's kind and subject;hubtest.Hubanswers 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,sshandssh.*, asconfig.DiffBasenames 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.requestno longer reset it, so an agent pollingstatusno longer keeps an unattended console open untilmax_session(#10). mcpkit.Exact(and soExactRows) also turns a plainintoruintoutside ±2^53 into its decimal string, as it did forjson.Number,int64anduint64: 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'sexactCell) should callmcpkit.Exact.- A client's
pii.addis audited with the client behind it (peer_uid,peer_pid,peer_user) and the pattern added (newaudit.BasefieldPattern, JSONpattern), on thetightenedrecord and on anyfailed/unsavedrecord 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 takey. When the start-up review ofconsole.Runwas approved with the name, the secondType 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): theDecisionof 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.Runasks 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 (recordConfirmedKey(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;Runturns it on):lox attachand lea show the pending question instead of guessing it (#127).
v0.12.0 (2026-11-02) #
- New risk class
ticket.RiskGrant(grant), betweenread-piiandwrite: 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 (PlanHashis the hash of its canonical JSON, so the signed decision binds exactly that scope),Screenshows it, the lock enforces it. A grant ticket waits 4 h (TTL,ClampTTLmay only shorten it) and is alwaysFlags.NeverAuto:NewTicketsets it andValidate(soSignandVerifyTicket) refuses a grant ticket without it; the console keeps it on the tickets it pushes. New fixturesticket-grant-v1.jsonanddecision-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)andclient.SocketPathInstancedial<shared socket dir>/<instance>/...(project key, profile and peer check unchanged; instance""isDial/SocketPath, byte for byte);client.SocketDirInstanceis 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 inLEA_<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--instanceor<PREFIX>INSTANCEbeside a single lock),DialInstancesays so and names both sources instead of thelox uphint. Each Lock wires--instanceinto its client commands andmcp(GUIDE.md §7).
Breaking: the console asks the Vault token at its start.
console.Options.VaultReadnow takes the token:func(ctx context.Context, path string, token []byte) (*secrets.VaultCredentials, error)(it wasfunc(ctx, path)); the default issecrets.VaultReadToken. A caller that sets it must take the token.- With
credentials = "vault"the console asksVault token for <VAULT_ADDR>:at its start, masked, and keeps the token in memory only, wiped when the console ends. It never readsVAULT_TOKENor~/.vault-token, so a console started bylox upcan 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.StandaloneReasonandconsole.Withdrawnare gone (deprecated since v0.9.0,WithdrawStandaloneinert since v0.9.1; no caller left in the Locks or lea).ipc.CodeWithdrawnstays, 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
verbsand GUIDE.md §14, the launcher verbs contract thatlox initdrives every lock with:<lock> init <agent>,<lock> profile add [--non-interactive] [--json] ...and<lock> doctor [--fix [--yes]] [--json].verbs.Parseparses a non-interactiveprofile addwithout ever quoting an argument in its errors (the flag package's would), thenverbs.RefuseSecretsrefuses 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.WantsJSONfinds--jsonin arguments that do not parse;verbs.Credentialsacceptsask(the console asks at start) orvaultwith a checked--vault-path, and refuses any other source (the keychain included). Both wrapverbs.ErrSecret.verbs.Profile,verbs.Error(codesusage,secret,exists,failed) andverbs.Doctor/verbs.NewDoctorare the JSONlox initreads (#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 upstarts its console without hub flags).Runstarts without hub options and refuses them with it (console.ErrPolicyOnlyHub), and refusesOptions.OnHub; it opens no hub. Every request handed toEnv.Approveis refused (console.ErrPolicyOnly,ipc.CodeDenied, auditedrefused), 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 underlox 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.StartCommandislox up(its arguments are no longer used), and theclient.NoConsoleErrorhint isclient.StartHint, "the console is started bylox uponly: ask the user to runlox up", with no profile or project in it:<lock> console --profile …refuses to start since v0.9.0.mcpkit.NoConsoleanswers the same for a bareclient.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.Runrequires its hub options: without--hub,--hub-key-fileand--instance(or with only some of them) it refuses before any check, prompt or file write withconsole.ErrNoHub("the console is launched bylox uponly"). The standalone start, which withdrew the plans hub mode had left pending, is gone, and so is theconsole.Serverrefusal of a run by ticket without a hub (#112).- Auto mode is gone:
Options.Auto(--auto) is refused byRun,ServerConfig.AutobyNewServer; Shift+Tab and:modedo nothing, the status row and:statushave no mode. Requests are decided on lockhub (whose own auto mode honoursScreen.NeverAuto); the console's own decisions (a policy change,:unmask) are still asked on its terminal, by the approverhub.Approverwraps (#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 autoWords(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.Newand the console's--hubcheck refuse a path (https://h//included), query, fragment, IPv6 zone, a host that is not a DNS name or an IP address (url.Parsetakes;&=!$'()*+,in a host) or a port outside 1-65535, as they refuse a user or password, without quoting the URL.hubclient.CanonicalURLreturns that form,Client.URLthe hub as the client prints it: the console's start line uses it, andconsole.HubOptions.Argspasses the hub on in that form only (an invalid--instanceor--require-deviceas a placeholder the new console refuses). A hub that cannot be reached is anErrUnreachablenaming 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--profilemismatch,ipcsocket names,sysconf'sservice_userandclient_grouplookups, and the console's wrong-account and console-is-a-client refusals (they nameservice_user,client_group) (#107). - New:
ticket.RiskNames(#107).
v0.8.0 (2026-10-31) #
pii:FindInViews,FindandDetectionView(s): detected values as ranges with their kind, also behind percent, JSON, HTML,\xHH,\e,\033and 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/textv0.43.0 (unicode/norm);golang.org/x/syncmoves to v0.24.0.pii: two opt-in detectors,ip(public IPv4 and IPv6 addresses, IPv4-mapped) andsecret(credentials, tokens, private keys, masked as<redacted>and run first), andpii.LooksSecret(name). Neither is inAllDetectors: a product masks exactly what it masked before until it names them (GUIDE §8, #96, #97).pii: every detector reads a\n,\ror\tescape, and the\u0026-style escapes of&,<,>,=and quotes (JSON written by zap, logrus, bunyan or Go'sencoding/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 keepAllDetectors(#97).pii: thesecretdetector 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\u0022escape 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),NewEpochandOpenEpochStore(private 0600/0700 key files, one process per directory,Rotate,RotationDue,Prune) (GUIDE §8, #95, #98).pii: opt-inperson_namedetector (gazetteer of given and family names, titles), not inAllDetectors(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.ClaudeAllowProjectmergesClaudeAllowinto the project's.claude/settings.local.jsonon a projectInit(off by default; GUIDE §12, #103).agentwire: mergedpermissions.allowentries (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.ParseSSHnamesssh.credentialsand the allowed modes,pii.Detectorsthe entry's position and the known detectors,sysconf.Loadthefingerkey, andhubclient.Newthe hub by scheme and host only (#99). hubclient.Newrefuses 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
:reviewwhose 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.Connectgets the policy in force:Credentials.Policy(#12), at start and at every reconnection (ServerConfig.ReconnectPolicy, used instead ofReconnectwhen set;Runsets it).console.Unwrap: the backend's own session behind a Vault-leased one (expiringSession.Unwrap) (#13).- Optional backend interfaces:
StatusAnswerer(the wholestatusanswer, flat),StatusRowerandStatusLiner(:statusrows, 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;NewServerrefuses a start policy it refuses, audited byRun),Worder(Words: the console's lines in the product's terms; an empty field keeps the console's,NoAutoHubrefuses auto mode in hub mode). EndError: aReconnecterror that ends the session with its reason.AskerOf(env)andPolicyPendingOf(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. :reviewno 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 ituncheckedand 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 isnot_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 withunchecked(this release) before locks send it (#84).hubclient: a hubrenewrequest for a pending ticket whose approval aged past its TTL ends the plan Expired (ExpireApproval, asBeginwould 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 onceRunopened it, before any request: where a product registersSetRecompute(#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, kindsrevealandrenew) arrives in therequestsof the decisions long poll;ticket.RevealKeyis the facade's ephemeral X25519 key signed by its Ed25519 key (ContextRevealKey,NewRevealKey,Verifyagainst the lock'sPinnedFacades;CanReveal:tty,deckonly); the lock answersPOST /v1/requests/{id}/answerwith aRevealAnswersigned by its key (SealReveal,ContextRevealAnswer), which the facade opens withOpenReveal. The hub relays ciphertext only.FacadeSet,ParseFacades,hubclient.ReadFacades,Config.Facades, console flag--facade-keys-file(HubOptions.FacadeKeysFile, inArgs).SubmitClearkeeps 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,expiredorapproval-expiredwithapproved_at,checked,changes: the names of the plan-hash inputs that moved (changed/new/gone), never values nor digests;InputDigestsstays in the lock,DiffInputs,Unchanged). A changed policy fingerprint is no renewal (policy_changed: plan again).hubclient.Client.Renew(agent path) and the hub'srenewrequest make a new ticket for the same plan, re-checked throughSetRecompute;Beginanswers an expired ticketticket.ErrExpiredand 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 eventsreveal,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,ParseDevicesin canonical JSON,MarshalDevices,Fingerprint);Decision.Device, an optional WebAuthn assertion overDecision.Statement(every field a lock acts on, the approver's display name andTicketHashof the signed ticket, underContextDeviceDecision), present only with facademobile;VerifyAssertion(ES256, user present and verified, origin, rp id) andVerifyDevice(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;Openreads both algorithms.PII.SealedForcarries the clear screen sealed to each pinned device (SealClearScreen, up toMaxClearScreen;OpenClearScreen), omitted otherwise. Fixed vectors intestdata/device-v1.json. -
hubclient:Config.Devices: a decision with a device signature, or from themobilefacade, runs only if it verifies under a pinned device over the lock's own ticket (checked inBegin; the decision is dropped otherwise).Config.RequireDevice: approvals of those risk classes without a device signature end the plan Denied.Config.SealClearwithSubmitClearseals the clear screen to the devices.ReadDevices,ParseRisks. -
consolehub mode:--devices-file,--require-device,--seal-clear.HubOptions.Argsrenders 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
--hubwithdraws the plans its hub mode left pending (console.WithdrawStandalone) and answers a run of a hub ticket with the newipc.CodeWithdrawn(console.WithdrawnResp,console.TicketParam) before the product sees it.plans.Store.WithdrawAllmarks the plansUntold(kept past the retention until told);hubclient.Client.Syncwithdraws them at the hub on the next start in hub mode (Untold,MarkTold).hubtest.Hub.SetWithdrawStatusfor 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:RequesterFromEnvsends the hash ofLEA_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): anhttp://hub URL is refused unless the host is a loopback address, orConfig.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, theLock-*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.AllDetectorspanics rather than return none. -
plans(#60):Openlocks the store directory (flock, LockFileEx), without waiting: a second store on the same directory (two consoles on one volume) fails withErrLocked, so a decision nonce is accepted once.Store.Closereleases it;console.Hub.Closetoo, andRuncloses 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 (everyprev_hashremoved, whichVerifyalone reads as legacy), is a*ChainError.Summary.Anchor()makes the anchor. -
render(#58):MaskScreenwith no detectors runs every detector (pii.AllDetectors: all but the opt-inphone_loose), never none: a product that forgetspii.Detectorssends 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).Looprecovers a panickingHandletoo. -
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. -
consolehub mode (#28):HubOptions(--hub,--hub-key-file,--instance, all three or none,--autorefused with them) putRunin hub mode: requests with aScreen.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/hubtestis a fake hub for tests. -
audit:prev_hashis 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 namingprev_hashitself 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.Screencarries the approval screen to the approver (a terminal approver ignores it). AnAsyncApprover(lockhub) takes requests withSubmit: the request is answered pending (PendingResp:{"status":"pending","ticket":…,"lock":…}, a result, not an error), set asApproval.Pendingand asApproval.Refusalso 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 (Newrefuses a config without one; no discovery).Submittakes arender.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;Syncresends.Poll/Watchlong-poll decisions with a cursor kept in a 0600 state file with the decisions not yet acted on;BeginchecksMatch,Usableand the plan's binding, then consumes the decision nonce and returns the verified decision (approver, facade, signature for the audit);FinishandWithdrawtell the hub. -
audit:EventPending, the record of a request handed to the hub (ticketset), 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 orScreen.Recordreturning no valid record, is refused withipc.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:reviewor 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 structuredPendingResult{"status":"pending","ticket":…,"lock":…}.RequesterFromEnvreads lea'sLEA_AGENTandLEA_AGENT_TOKENinto the newipc.Requester, carried in plan and run params;ipc.CodeWrongRequesterrefuses a plan or ticket to another agent's token. -
audit(#30): hash chain and hub fields, additive. Every record now carriesprev_hash, theLineHashof the line before it,Genesison the first line of an empty log;Verifychecks a log and returns a*ChainErrorat 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.Basegainsticket,approver,facadeandhub_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 runsudo <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;Checkmarks it stale when either hash moved,Beginruns it once under a decision nonce recorded durably (ErrReplay,ErrUsed).FindPendingdedupes 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 byMaskScreen(ScreenSource, Masking): values in personal columns (pii.LooksPersonalColumnorMasking.Personal) masked whole, the detectors over everything else, the structure of the request kept.Screen()is theticket.Screen,Touched()the kinds masked; the zero value is notValidand does not encode (ErrUnmasked). -
ticket:Decision.Usablerefuses a decision that is not an approval (ErrDenied). -
ticket(new, #25): the contract between a lock and lockhub. SignedTicket(lock key) andDecision(hub key, carrying the ticket's instance, lock key id and plan hash:Matchrefuses a decision for another instance or plan), closedRiskclasses with their TTL (reads none, write and policy-loosen 24 h, prod-write and unlock 4 h;ClampTTLonly shortens), RFC 8785 canonical JSON (Canonical,CanonicalizeJSON), Ed25519 signatures with domain separation (SignBytes/VerifyBytesandContext), strict verification, X25519 sealed boxes (Seal/Open) and signed X25519 keys (KeyCert), ULID ids, nonces,HashOf,RequestMessage(whatLock-Sigsigns). Only the exact canonical form verifies;Canonicalrefuses integers a double cannot hold and invalid UTF-8 rather than encode them with a loss. Signed fixtures underticket/testdatafor lockhub's compatibility tests. -
console:Askertakes a console's human decisions:Askcloses an approval screen with the prompt (typed profile name on production, retype answer, fingerprint) and returns theApproval, its outcome recorded through a callback;Decideasks for any other decision.Server.Approveruns 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.Fornames 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.RefuseProductioncarries them;Runsets it.
v0.2.1 (2026-10-28) #
pii(locksql#18):LooksPersonalColumnalso reportsmail,e_mail,login(unless the column is a date or time, as Django'slast_login),username,nickname,displayname,pseudoandhandle;username,nicknameanddisplaynamecount inside unsplit compounds likefirstname.LooksQuasialso reports free-text columns (notes,comment,bio,remarksand their French, Dutch, German and Spanish forms) and coordinates (lat,lng,lon,latitude,longitude,coordinates).neutralLaststill 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) isErrVaultAuth, whose hint asks for another token rather thanvault login; the token is cut out of the CLI's message.VaultReadis 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-inphone_loosedetector for national numbers without a trunk 0;Detectscans integral floats;CutAtalso 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):RunreturnsErrConfigforShowResultsat level 1,NewServerrefusesShowResultsat any level but 0, and Ctrl-O /:resultsleave the results masked at level 1 withresults stay masked at level 1: the agent's account can read this window; a refused switch is not audited.--autoand--allow-unmaskare 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, structuredInfo;E_POLICYbuilt 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.tomland 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,Policywith 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, theBackendseam,PIIBackendforpii.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.