Changelog · Changelogs
locklog changelog
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses Semantic Versioning.
Unreleased #
0.3.0 - 2026-11-13 #
The company's managed configuration (managed.toml), lockkit v0.19.1
(from v0.13.1) and faster JSON decoding. A minor release (pre-1.0 semver):
it adds a refusal no human can approve past, and the MCP tools refuse calls
they used to answer.
Breaking #
- With a managed configuration (
/etc/lockfamily/managed.toml), a profile beyond its[locklog.bounds], or a profile of yours named as a managed one, is refused at the console's start and at every policy poll, and the console exits 3 (see Added). Without the file, nothing changes. - MCP (#144): a docker source's
containeraddressed as a field (where,fields, countby) is refused, naming the label to use (it was answered with nothing), and a cursor given with other parameters than the first call's is refused naming the ones that differ.locklog_counttakesby_labels(a docker source's container, Loki labels); a cursor continues when the range is left out (the first call's window), and the tool descriptions say to repeat the parameters.
Behaviour #
- lockkit v0.19.0 (from v0.14.1, #147). The default value detectors mask
more: a French NIR is recognised before a card number,
phoneis wider,be_nissis new and an e-mail address may hold an apostrophe. A URL holding a user (https://u@host) is now refused by lockkit's check ("url looks like it embeds a secret"), before locklog's own. Thelocklog inittext says the allowed calls answer within the console's policy and budgets. - lockkit v0.15.0–v0.18.0, through the hub client every lock shares: the
console opens at most 30 tickets a human decides per 5 minutes (was 10)
and 30 pending ones (was 20), a ticket
lox run's auto rules decide leaving that count (a safety cap of 300 per window counts every one);readandread-piitickets, and their approvals, expire after 1 h (was never); the status answer no longer carriesauto_approve, which locklog never showed the agent. - lockkit v0.14.1 (from v0.13.1).
locklog consolewithout its hub flags now refuses with lockkit's "the console is launched bylox runonly" (waslox up, lockkit#147); docs/usage.md quotes the new text. - The launcher mounts the machine's managed directory (
/etc/lockfamily,/Library/Application Support/lockfamilyon macOS) read-only on/etc/lockfamilyin every container it runs, aslox rundoes in its locks:locklog managed show,managed check,doctorand the other commands run from the host see the company's managed configuration. A managed path that is not a directory, or that a docker mount cannot carry, stops the launcher (#146).
Performance #
- JSON lines are decoded about 8 times faster (#150): locklog reads them with a scanner of its own instead of encoding/json's token reader, exactly as before (the same strings, numbers, member order, depth and key limits). With lockkit v0.19.1 (its detectors skip text that cannot hold a value), an approved scan of JSON logs runs several times faster.
Added #
- The company's managed configuration (
/etc/lockfamily/managed.toml, the Lock Family'smanaged.toml): locklog enforces its[locklog]section (#147).[locklog.profiles.<name>]are served as written and cannot be redefined in the config files;[locklog.defaults]fill the keys a profile leaves unset;[locklog.bounds]bound every profile (production, structured, text, unmask, lookup, strict, prefilter, user_journals, pseudonym rotation and max age, auto rules, every limit, and the allowlists kinds, hosts, paths, auth, credentials, tls, network, extra_roots): a profile beyond them is refused at the console's start and at every policy poll, with no prompt, citing the managed key, and no human can approve past it. Requireddetectorsand[[locklog.bounds.pii_mask]]rules are always on; a project[[allow]]that could show what a managed mask hides is refused.[family] vault.requiredrequirescredentials = "vault". Keys: docs/usage.md. locklog managed showandlocklog managed check [--profile NAME] [--project DIR](exit 3 on a violation, oneblocked by company policy: …line each;lox runruns it before it starts the lock), amanagedline inlocklog doctor, andlocklog consoleexits 3 when the managed configuration refuses it.
Fixed #
- The launcher picks BSD (
stat -f) or GNU (stat -c) stat by probingstatinstead of readinguname -s. On macOS with GNU coreutils first inPATH, the project directory was silently ignored (GNUstat -fis the file system status); a Linuxunamewith BSD stat refused it (#152). Tests: thepii.addaudit tests no longer need a journal directory, so they pass on macOS.
0.2.1 - 2026-11-07 #
Fixes and distribution: no new console method or profile setting.
Changed #
- Releases ask the Homebrew tap to bump the
locklogformula once the release is published (tapjob; needs theTAP_DISPATCH_TOKENsecret, skipped without it). The install docs lead withbrew install davidgodefroid/tap/lockfamilyandlox add locklog/lox run, withinstall.shas the alternative (#134). scripts/install.sh's last hint has lockfamily's wording:lox add locklogin your project, thenlox runin a terminal of its own, outside the agent, where approvals live (#134).- lockkit v0.13.1 (from v0.13.0).
Fixed #
- In hub mode, a console with local sources only accepts exactly lox run's
internal hub network: one interface besides the loopback, the one its
--hubaddress is routed through, with no default route and no route through a gateway. It refuses a second interface, a network with a route out, or a network the hub is not on. lox run passes nothing new (#136). - The console audits the backend's
pii.addrefusals with the client's peer (lockkit v0.13.1), and locklog no longer records them a second time (lockkit#142). - Tests: the leak suite no longer fails at random (about one run in five)
when the random scan id held a 7-digit run of a corpus phone number; the
whole-corpus scan blanks the scan id only in its exact 24-hex form
(
TestLeakScannerScanID). No production code changed (#138).
0.2.0 - 2026-11-06 #
Hub only: the console is started by lox run (lockhub) only, and locklog
takes the launcher verbs every lock of the Lock Family takes (#117).
lox run starts a locklog console once lockhub knows the locklog type
(lockhub#84, #86, #87); until then the e2e and CI jobs start it the way
lox run will, against lockkit's test hub.
Added #
-
Hub mode (LL-23):
locklog consoletakes lockkit's hub flags (--hub,--hub-key-file,--instance,--hub-plain-http,--devices-file,--require-device,--seal-clear,--facade-keys-file; with a devices file,read-piirequires a device by default), andlocklog hub-key --instance NAMEprints the instance key forlox runto pin. A scan, an unmask or an approved lookup becomes a plan bound to the peer, the agent's token, the policy and the engine's keyed plan hash, pushed to lockhub with a masked screen; the clear screen, secrets redacted, goes sealed to the pinned devices only. The agent hearspendingwith the ticket id only (no plan hash, digest or policy fingerprint) and runs the same call withticketonce decided: the engine plans it again and it runs once if the keyed plan hash is the ticket's (E_STALEotherwise). Aread-piidecision from a facade other thantty,deckormobileis refused by the console. Expired tickets are renewed; decisions, renewals and reveals are audited; with the hub down, free reads go on.locklog_statusshows the hub mode and instance. The requester the hub shows is the kernel-verified peer uid, never a name the agent declares; an approved method without peer credentials is refused in hub mode. In hub mode a console reading only local logs accepts one network interface, the one it reaches lockhub on (lox run's internal network), and still refuses any other. -
Tests (LL-16b): the leak, oracle, equivalence and injection suites run over a Loki source too (a fake Loki in unit runs, a real one in the CI
suitesjob), and over the approval tools (approved scans and their continuations, unmask, lookup, change requests): no canary in any answer, refusal or approval screen (the clear view of an unmask screen, the human's own, holds no secret), and an unmask reveals only the span approved.TestLokiIncident(CI jobloki-incident) runs job 1 against a real Loki with a dense incident hour over a 7-day background, with and without alevellabel, behind a latency proxy and on a slowed host; the ingest overrides it needs are indocs/loki.md. The approved-scan test (CI jobscan-volume) scans 20 GiB of local files on one approval. -
A leak harness over the bytes leaving the console: the canary corpus served by a hub-attached console, its IPC socket replies, audit file, MCP outputs and hub ticket screens scanned in every encoding: 0 hits (#86).
-
On a production profile without Landlock,
:statusanddoctorname the residual: its local sources are read by an unconfined console (#62). -
Auto mode per profile for
lox run(#126):auto = "full-auto", or an[profiles.P.auto]table settingread(an approved scan),unmaskandlookup(an approved lookup) toauto,askordeny(defaultaskfor all three: unchanged behaviour).denyis refused by the console before any ticket (E_AUTO_DENY;lookup = "deny"refuses free lookups too);asktickets are signednever_autoand an auto rule's approval of one is refused;autotickets are left tolox run's auto mode. The rules are part of the fingerprinted policy (stored sparsely: approved policies keep their fingerprint); moving one towardautois a reviewed loosening, with the typed profile name on production. Shown in the review,:statusandlocklog_status(auto); the audit recordsauto_ruleanddecided_by. Newlocklog profile rules [--json] [--project DIR] [--profile NAME], whichlox runreads. -
docs/recipes/holmesgpt.md: HolmesGPT's CLI in its own container with the locklog client and only the lock's socket volume from
up.json, locklog as its stdio MCP server,MCP_TOOL_CALL_TIMEOUT_SEC=600; a test checks that the recipes' tools, ids and volume match the code (#37). -
locklog_pii_listandlocklog_pii_addare served: an agent rule ([pseudonym ]source:field, a masked key by its⟦key:ID⟧) masks a field the agent was shown, from the next call on; it is capped bypii_adds_per_hourandpii_rules_max, stored as a keyed digest in the console's home volume only (never in.locklog/pii.toml), audited accepted or refused with that digest and never a name, and counted inlocklog_statusand:status(agent_rules) (#64). -
The allow rules of
.locklog/pii.tomlare part of the reviewed policy: an added or widened one is a loosening approved at:review, a removed one applies at once, and the engine applies only the approved ones (#61). -
The unmask approval screen shows, for each pseudonym, how many records holding it agents were given in this epoch (counted by the console from the calls it answered and audited, never from clear values; #91).
-
locklog_statusand:statusshow a docker source's containers map (ok,none,invalid), the containers served and the stale ones (removed, still mapped); andbudgets.lookups_left_peer, the lookups left to the calling agent's uid. -
An approved scan re-attached after its last answered page says
"resumed": true; a continuation with the cursor of a page answered already is refused saying so.
Changed #
- A log root at, beneath or above one of the console's writable
directories (an extra root under
/tmpor the home volume) is refused instead of losing its read-only Landlock rule (#62). - Hub tickets are version 2 (lockkit's kind
read, the profile as their subject), so thatlox runcan apply a profile's rules to them (#126). - Breaking. lockkit v0.13.0 (from v0.8.1).
locklog consolewithout--hub,--hub-key-fileand--instancerefuses before it touches the terminal, the project or the config, with lockkit's "the console is launched bylox uponly" (lockkit's text predates thelox runname). There is no standalone console and no auto mode. - Breaking. The launcher runs no console:
locklog consoleandlocklog hub-keythrough it say thatlox runruns them and exit 2. Its console path (the mount plan, the containers map and itsdocker eventsloop,refresh, the hub-file copies of #114) is gone:lox runmounts the plan (lockhub#86). - Breaking. The launcher's
mcpreaches the locklox runruns: it finds.lockfamily/up.jsonup from the current directory (the user's, no link, not writable by group or others, at most 1 MiB, version 1, this workspace's), checks the socket volume's labels and the containers that mount it with Docker, and mounts it read-only on/run/locklog; with none it says to runlox runfirst and exits 2.--instance NAMEpicks one of several locklog locks (without it the error names them).doctormounts the same volume when there is one. - Breaking.
locklog setupis replaced by the launcher verbs of lockkit's GUIDE.md §14:locklog profile add [--non-interactive] [--json] [--project DIR] [--profile NAME] [--production] [--only DIR,...] [--loki-proxy SERVICE ...] [--credentials ask|vault [--vault-path P]](a profile from the host's logs, appended to an existing config, never rewriting one:exists; never a secret: lockkit'sverbschecks),locklog init <agent>(now run as you, the project writable), andlocklog doctor [--fix [--yes]] [--json](also checks the agents' wiring;--fixwires the ones installed here). The own-container recipe and the lea placement are no longer printed: the recipe is in docs/setup.md. - An approved scan's ticket is
never_auto(still riskread, one approval for its whole plan), like an unmask's and an approved lookup's:lox run's auto mode approves no locklog ticket, and the console refuses an auto rule's approval of one (lockkit'sErrAuto). locklog mcp --instance NAME(elseLOCKLOG_INSTANCE, else the one lock lea lists inLEA_LOCKLOG_INSTANCES) dials one lock among several (lockkit'sclient.DialInstance).- From lockkit v0.11 to v0.13: the console asks the Vault token at its start
for a
credentials = "vault"source (neverVAULT_TOKENor~/.vault-token); a production profile's widenings at the console (the policy review, a loosening at:review,:unmask) take the profile name typed exactly; the idle timeout counts only the human's input and the backend's methods. - Remote sources still connect at the console's start (no
connectticket, lockkit'slazyconn, yet). - CI: the five-minute, own-container, production-guard and e2e jobs start
the console as
lox rundoes, against lockkit's fake hub, untillox runknows locklog (lockhub#84, #86, #87); the docker-source job writes the containers map itself. - In hub mode an approved scan is bound to the agent's token as well as its uid: another agent of the same uid neither re-attaches to it nor continues it (#115).
- An approved lookup (
lookup = "approved") is charged to the lookup budget once approved, not when asked: a denied or pending lookup uses none of it (#98). log.sourcesdescribe requests draw on the remote request budget (since 0.1.0) and now have the call deadline: with the budget used up they are refused at once instead of waiting for the window.- A free remote call whose request budget has no room before its deadline is refused before any request, with its estimate.
- A scan's reason is masked on its approval screen and in the audit, as an unmask's or a lookup's; a patterns scan against a baseline names the baseline window on its screen; a renewed scan ticket names its inputs unchecked instead of unchanged.
Fixed #
- A Loki call's
scanned.bytes(and the bytes the rolling budget charges) leave out thestatsblock of Loki's answers, whose timings changed them from run to run (#111). - A Loki count's boundary log fetches are charged as requests, not as server-side metric queries: a 7-day count by a trusted label costs 4 of the 120 hourly metric queries instead of up to 132 (#112).
- A count with only trusted fields and labels masks no record whose trusted values the masker leaves unchanged when it scans what the index does not hold: it counts them by those values, as the index does, at parsing speed (#94). Such records are no longer masked failures.
- Masking is about 2 to 2.5 times faster on JSON application logs: clean
field names, clean trusted values and the decisions a leaf's name and
path take are cached, values withheld whole (redact-unknown, redact
rules) skip detection, agent-rule digests are memoized, and a group of
records is masked while it is read (#104). Job 1 of the Loki incident
without a
levellabel answers within its 5 minutes (#110). - An approved scan whose plan is long lives as long as its plan's bound
(twice its time at the cost model's worst-case rate) instead of 30
minutes, at most 4 hours; its approval screen and
expires_atsay so. A 20 GiB scan of local files then completes on its one approval (#104). - The confined console resolves host names again: Landlock now grants
/etc/hosts,/etc/resolv.confand/etc/nsswitch.conf, which Go's resolver reads at every lookup (remote sources and the hub were dialled by name to[::1]:53), and the controlling terminal for a secret asked again (#62). - A containers root the launcher could not check from the host is pinned by
its device:inode at the first connection and checked on every call; a
file, journald or docker source whose root is gone, stale or replaced
ends the call as a lost session (
CodeConnLost), not a refusal (#62). - The MCP tools no longer echo what the agent sent in the SDK's argument errors (schema validation, an unknown tool's name): a fixed text names the argument and the rule (#86).
locklog doctorbefore any profile checks the account as for a production profile (red, exit 1) and says to runlocklog profile add; it exited 0 (#87).- An AD/SSSD group with a space (
docker users@corp.example) no longer reads as the docker group: the launcher's facts name each gid whole (id -G,getent group); without them, a matching piece stays a finding that says the name may be split (#87). - Each cloud's metadata line and remedy are its own: Azure's speak of its metadata service, not of credentials, and only AWS's names IMDSv2 (#87).
profile addwrites.locklog/through the directory it opened and checked after the prompts: a.locklog/replaced by a link meanwhile is refused, never followed (#87). The launcher's log paths are NUL-delimited (find -print0): a newline in a name cannot forge a path.- The engine runs at most
limits.max_callsagent calls at once (default 4); one more is refused at once withE_BUSY(#50). A call's deadline covers its planning: a plan cut by it is refused as over budget, and a call left no time stops before any work with its cursor (#49). - The output cap bounds every answer: a record larger than it comes back
as an
oversizedstub with its ref (#48), and a count whose groups do not fit is paged with a cursor (a partial one is markedtruncated) (#47). A search without predicates whose matches outgrow the page cache continues with its cursor instead of being refused (#46). - The split of an over-budget remote slice drops its narrower slices outside the query's range (a window's and its baseline's for patterns) before reading them: a dense second just outside the range no longer refuses it (#73).
- The combined (access log) format no longer refuses a valid line whose three leading fields, up to 255 runes of up to 4 bytes each, put the timestamp past byte 800 (#85).
- An agent rule added while the local index builds a granule redacts the records indexed after it: the index's memo of trusted values the masker leaves clear follows the rules' generation (#84).
- A descending journald search resumes the journal's entry-array walk at each granule instead of walking the journal from its head per granule (#51).
- Numeric terms (
status = 500,in [200, 5e2],!=) are served from the local index and prune its granules; a granule holding the number written otherwise (2e2for200, which matches as a JSON number and not as a string) is scanned. Trusted-only counts with them get the count range (#52). - A source failure is printed on the console's terminal (method, source, the class the agent gets and the error, masked and sanitised; never a record); a stale stream is not (#54).
- A
kind = "file"source incriordocker-jsonformat refuses a trustedattrsorattrs.<key>, which it never keeps (only a docker source does); itsstreamstays a trusted label by default (#55). - The documentation says when
masked_failuresis not counted: a text search with the opt-in pre-filter (#53). - Hub mode: an unaudited call withdraws only a ticket it submitted itself,
never an earlier call's pending ticket it was lent; the plan of a
ticket's run is finished even when the engine panics; a call without
peer credentials that needs no approval (a free lookup) runs, one that
needs an approval is refused with
E_APPROVAL. - An approved scan over several remote sources paces each source under its own name.
Not in this release: per-profile auto rules for lox run
(#126).
0.1.0 - 2026-11-06 #
The first release: the console, the engine and the MCP server over files, journald, Docker and Loki logs (Elasticsearch and OpenSearch as a preview), with the masking pipeline, the local index, budgets and scan approvals, and the site locklog.dev.
Still open in this release:
- The Loki and approval suites and
TestLokiIncident(LL-16b, #101) are not merged; their runs so far found:- without a
levellabel, the Loki incident's job 1 answers in 5m22s against its 5-minute gate (#110); - one 7-day trusted Loki count uses 98 of the 120 hourly metric queries, so job 1's next count is refused over budget with the default budgets (#112);
- a 20 GiB approved scan does not finish within the 30-minute approval (about 4 MiB/s measured; #104);
- a Loki call's
scanned.bytesvaries between identical runs (#111).
- without a
- On a cold start (no local index yet) a trusted count scans the not-yet-indexed rest with full masking, which misses its gate on a loaded host (#94).
pii.listandpii.addanswer "none" until the console serves the PII backend (#64).- Remote sources (Loki, Elasticsearch, OpenSearch) have open review
follow-ups: off production a failed Elasticsearch check still pushes the
configured selector, remote connections are not closed, refusals can
quote strings the server sent, the connection checks are outside the
request pacing and budgets, and relative or
~TLS paths pass the config check when the console runs without the launcher (#106); the Elasticsearch check and scan liveness follow-ups of #93. - The leak harness checks every agent method's output; the bytes of the console socket, the audit and the MCP outputs are not scanned by it yet (#86).
Added #
- The console's clear view: with clear results on (Ctrl-O, lockkit's
switch), the records an agent call returned are printed on the console's
own terminal with their pseudonyms in clear (secrets, redactions and
masked field names kept), counted in the audit (
shown_clear), never sent to the agent; an unmask approval screen shows its record in clear there too, and says Ctrl-O shows it when the switch is off. - Repository bootstrap: Go module,
locklog version, CI (tests on Linux, macOS and Windows; hygiene, gofmt, vet, staticcheck, govulncheck). - Configuration:
.locklog/config.tomlprofiles (version 1) with file, journald, docker and Loki sources, masking modes, budgets and their ceilings, production rules, and the policy diff that marks every loosening for the console's review. - The image (
ghcr.io/davidgodefroid/locklog, accounts 10050-10052,-vaultvariant), the host launcherscripts/locklog(the console's confinement and read-only, non-recursive mount plan checked on the host;mcpas the agent account;update,version),scripts/install.sh(cosign-verified archive and image, root-owned launcher, snapshot release bases), and the release workflow (draft, signed images, anonymous pull, publish, pages). - Egress for remote sources: an HTTP client pinned to the configured host and port (no proxy, no redirects, link-local addresses refused), the Loki read-only allowlist checked segment by segment, response caps, the tls modes, and the production check that refuses a backend answering a data read without credentials.
- The console (
locklog console): the agent's log methods served through lockkit's console over a seam to the engine, the launcher's checks before any log is read (dev:inode of.locklogand of each mount root; no network for a local-only profile), an audit record per call written before the answer leaves (fail closed; never a predicate or a value), and--allowed-uids. - The console opens the engine of the approved policy: a masker per source
on one pseudonym epoch (rotated and pruned as the profile says),
K_refand the local index in the home volume (a missing or corrupt key refuses the start), the file, journald and docker sources given only the mount roots the launcher checked (every planned root must be inLOCKLOG_EXPECT_ROOTS, the containers root may be the launcher's "not checked from the host" case), the calibrated cost model, the profile's per-call budgets, andlog.status/log.sources. In its image the console confines itself with Landlock before it opens any source, and a docker source on a production profile needs Landlock or openat2;:statusshows the Landlock state. The mounts are checked again on every call, and a session lost under a call ends it with a connection-lost error (the next call reconnects). - The MCP server (
locklog mcp):locklog_status,locklog_sources,locklog_search,locklog_count(alsoby_pattern),locklog_patterns(the console now serveslog.patterns), the PII rules and change requests, each a thin adapter over the console; a host agent (the launcher'smcp) is refused on production profiles, as the console's status reports them, with the structured refusalE_PRODUCTION_HOST_AGENT(and when the status does not say: the guard fails closed). Its status tool never shows that profile's status, and no tool's structured output carries more than its text. Inputs are bounded by their schemas; integers beyond 2^53 come back as decimal strings. The PII tools answer once the console serves PII rules.locklog_pii_addis free (it only masks more), on production too for an agent that is not a host agent.locklog init <agent>wires Claude Code, Codex, Cursor or Gemini CLI; for Claude Code it merges the free tools (never an approved one such aslocklog_request_change) into the project's.claude/settings.local.jsonpermissions.allow, keeping the human's entries; the shared.claude/settings.jsonand the user's own settings are never written. A re-run adds back the entries the human removed, and prints each entry it added (added to permissions.allow in .claude/settings.local.json: <entry>); remove an entry again to be asked for it. init warns when git does not ignore.claude/settings.local.json. - The Docker source: containers'
json-filelogs (rotated.N[.gz]files included) read by name from the containers root through a name → id map, approved globs checked again at read time, console containers never served, no read across a mount point; thedocker-jsonformat, and anattrsallowlist on thedocker-jsonframes. The launcher binds the containers root with gid 0, keeps the map current with adocker eventsloop, labels console containers, and gainslocklog refresh. gid 0 applies to the whole console, so a docker source runs in a console of its own: the mount plan and the launcher refuse it beside a file or journald source. internal/landlock: a Landlock ruleset the console applies to itself (read-only beneath the log and containers roots, writes only to its volumes and /tmp, rights chosen by ABI), and the rule that a docker source on a production profile needs Landlock or openat2 withRESOLVE_NO_XDEV.- Parsing: the
syslog(RFC 5424 with structured data, RFC 3164 with or without PRI, rsyslog's RFC 3339 variant) andcombined(NCSA common/combined access logs) formats, detected byautoafter JSON and logfmt, and thecrianddocker-jsoncontainer frames (runtime time, stream, split lines joined, payload decoded withauto). - Masking on lockkit v0.8.0: its secret detector (credentials, token
shapes, private keys, URL userinfo) and
LooksSecretnames are step 1, always on and never a profile setting (detectors = ["secret"]is refused); public IP addresses (ip) join the default detectors;person_nameandphone_looseare opt-in by naming them in a profile'sdetectors. - Patterns (
log.patterns): the top templates of the masked message text (pseudonyms keep their kind,⟦email:…⟧; digit-bearing parts become<*>), with first/last times, an optionalbaselineoffset (the same window earlier: baseline counts,newandgonepatterns, each granule read once, even when a file grows between the two plans; the baseline's patterns capped apart from the window's,truncatedpast the cap; with a remote source both windows fit its range budget), plan-based admission and free continuations;countgainsby_pattern. - Engine read-around calls:
log.context(the records around a ref, before and after up to 50, the ref's record checked by its keyed digest),log.trace(one clear id or pseudonym over a range, whole words of the masked view only, with search's plans, budgets and cursors) andlog.tail(new records matching a filter for up to 50 s, at most 200), served by the console and as thelocklog_context,locklog_traceandlocklog_tailtools. locklog setup: the trusted.locklog/(yours, 0755, writable by no one else; checked again by the launcher afterwards) and a firstconfig.tomlfrom the host's facts, which the launcher gathers on the host (internal/hostfacts): log file families with their rotated and compressed siblings under one glob, the journal, the running containers (in a-dockerprofile of their own), never a console container. It asks whether the profile is production, and an unanswered question is yes;--yes --no-production --agent hostfor scripts.--agent hostrunsagentassets.InitProjectfor each detected agent (the Claude Code allow list in the project's.claude/settings.local.jsononly, nothing at user scope) and prints what it wrote;--agent containerprints the own-container recipe (a Dockerfile snippet copying the binary and a root-owned/etc/locklog/system.tomlfrom the image by digest, a non-root agent user without sudo in group 10050, the console account 10051 kept only as the name the client checks the socket's owner against, the run line with--cap-drop ALL --security-opt no-new-privileges,--allowed-uidsfor another uid, onelocklog-runvolume = one trust domain);--agent leasays where lea stands. The home directory is never a project: setup refuses it, and the launcher's walk ignores a~/.locklog/. The facts file is 0600 (doctor, which runs as the agent account, gets it on its input).--loki-proxy SERVICEwiresinternal/authproxyfor a Compose Loki: Caddy gets only a bcrypt hash, the token is shown once on the terminal (and refused without one), the CA key is discarded, the files go to~/.config/locklog/proxy/(0700, outside the project), and only the CA certificate is copied beside the config, fortls_ca.locklog doctor(advisory, not a boundary): the Docker engine andDOCKER_HOST, the groupsdocker,admandsystemd-journal,sudo -n, euid 0, a readable/var/log/journalor/var/lib/docker, cloud credential files, unauthenticated read probes of the configured backends and of the clouds' metadata paths, none of which issues a token (Azure: the instance document, "an identity may be available") (a status code and whether the API was recognised; never a response body), with a remedy for each finding; residuals: Landlock, containers withIpcModeshareable, swarm mode. On a production profile the findings are red and doctor exits 1. The launcher runs it as the agent account with the host's network.locklog setup --only DIR,...: detect only the log files under those directories (no journal, no container), for scripted runs.- CI end to end on the LL-09b harness:
five-minutes(the launcher on PATH,setup --yes --no-production --agent host, the console, and an MCP client spawning the server the written.mcp.jsonnames: an exact count and a masked search under 300 s, and the written allow list equal to the free tools),own-container recipe (e2e)(an agent image built from the printed snippet calls the console as uid 10052, not root, no sudo), andproduction guard (e2e)(the launcher'smcpagainst a production console is refused withE_PRODUCTION_HOST_AGENT). - Rolling budgets (LL-19), per source and for the console: bytes processed
per hour (plan-based, granule-rounded; remote: the bytes fetched),
remote backend requests per minute (paced, refused past the call's
deadline) and server-side metric queries per hour; search, count,
patterns, trace, context re-reads, tail polls and continuations all draw
on them and are refused with
E_OVER_BUDGETand the estimate before touching data.log.statusshows what is left. log.scan(locklog_scan): a search, count, patterns or trace over the free budgets, approved once (classread) for its whole plan. The plan hash is keyed (HMAC underK_ref) and binds the tool, the sources, the absolute range, the predicates, the budget and the policy fingerprint; the byte plan is frozen at the approval; continuations are free and need no new approval until the plan ends or the 30-minute TTL; the cursor is bound to the scan and the peer uid; a reissued scan with the same plan re-attaches; each call's deadline is min(240 s, what is left of 570 s since the request); a remote scan has its own request pacing, not the rolling per-minute budget.log.unmask(locklog_unmask): the clear values of exact pseudonyms of one record, once, on aread-piiapproval auto mode never gives; the plan hash binds the ref, the record's keyed digest, the epoch and the exact spans, checked again after the approval (E_STALE); secrets and values that held a secret span are never revealed;unmask = "never"(the production default) refuses withE_UNMASK_NEVER.log.lookup(locklog_lookup): the pseudonyms of clear values of a detector kind (equality only), refused for secrets, field names and template tokens; limited by distinct values per hour per peer uid and per console;lookup = "approved"makes it aread-piiapproval; audited by pseudonym only.- Plan hashes, query and record digests and the policy fingerprint never reach the agent (#26): a scan is known by a random id.
- The Elasticsearch 8 / OpenSearch 2 source (
kind = "elastic"or"opensearch"): allowlisted read routes on the source's own indices (_search,_count,_field_caps,_mapping,_settings,/,_security/user/_privileges,_resolve/index, OpenSearch'sauthinfo), each with its own query-parameter allowlist (the egress layer now checks parameters per route, Loki's included); typed query bodies (time range,term/termson trusted keyword fields, sort with an optional tiebreaker,search_after, size and timeout caps,date_histogramandtermscounts) with text matched locally, untrusted fields dropped, multi-valued fields and!=evaluated alike by the server and the engine (empty values kept local); the Loki paging and byte model (lazy slices, bytes counted per page against the call's limit, bounded Around and Reread,MaxSlices,Splitof an over-budget slice);auth = "api_key"(ElasticsearchApiKey). A production privilege check that fails closed: any grant beyond a read-only allowlist, on any pattern, alias or data stream, any OpenSearch role not known read-only (or vouched for inread_only_roles), an OpenSearch answer without a role, and a client certificate for OpenSearch are refused. Field checks over every index: trusted, selector and tiebreaker fields must be plain keyword fields (aliases judged by their target; normalizers, scripts, runtime fields andcopy_tointo them refused; dynamic templates accepted only when provably harmless;dynamicother than a boolean, true, false or strict refused);ignore_above,null_valueand non-standard index modes keep terms local. Until the newestCheckhas succeeded, no trusted term is pushed and no count is served on the server, and a stream whose pushed term is no longer server-side is stale. - The console opens Loki, Elasticsearch and OpenSearch sources (#96): the
egress client of each (its allowlist, the credentials the console asked
for or read from Vault, its tls mode), with the source's selector,
trusted labels, metadata and fields, indices and time, message and
tiebreaker fields. At connection the unauthenticated-read probe and, for
Elasticsearch / OpenSearch, the privilege and mapping check run; a
production profile is refused on either (an open endpoint, a
write-capable key), with an error that quotes no credential. Off
production a failed check is noted on
:statusand the source matches its terms locally until the console reconnects. Remote sources use the remote budgets: their range,limits.remote.count_rangefor counts the server answers, andlimits.remote.requests_per_minutefor every call,log.sourcesincluded. The launcher refuses a profile with a remote source withoutLOCKLOG_NETWORK;locklog doctorprobes Elasticsearch and OpenSearch mappings without credentials;locklog setupwrites a commented Elasticsearch / OpenSearch profile entry.
Fixed #
locklog console --profile P --allowed-uids UID(the own-container recipe's line for an agent uid the image does not know) works through the launcher: the mount plan (locklog mounts) gets the profile only.unmask = "never"refusals no longer name a:revealcommand; they ask for the console's clear view (Ctrl-O).locklog setup --agent hostwith no agent found points to--agents <agent>instead oflocklog init, which the host launcher cannot run.Scanned.Bytescounts the bytes a remote source fetched and the call dropped (out of a slice's range, past the call's byte limit, or more than the time left could mask), as the rolling budget charges them (#67).- A call naming no source has the remote per-call byte budget when a remote source is configured.
locklog_request_changebounds its text to the console's 500 bytes.- A Loki time slice holding more than one call's byte budget is split into narrower epoch-aligned slices (down to 1 s) and paged with a cursor, in both orders, instead of being refused; a slice still over the budget at 1 s is refused with a message that no longer speaks of compressed files.
- The launcher chooses the non-recursive bind option from the Docker CLI
version, which parses it (
bind-recursive=disabledfrom CLI 25), not from the engine's.