Authentication¶
3LC must be activated before it can write data. Activation validates a credential (an API key or a license key). Read operations do not require activation.
Activation¶
Activation happens automatically on import tlc from the configured credential:
API key — provided with
3lc login, theTLC_API_KEYenvironment variable, or the API key file. See API Key for details.License key — provided with the
TLC_LICENSEenvironment variable or the license key file; used with the 3LC Enterprise On-Prem deployment. See License Key for details.
TLC_API_KEY and TLC_LICENSE may contain either the key itself or the path of a file containing the key.
The advisory API: tlc.auth¶
tlc.auth is a read-only view over the current authentication state, intended for UX surfaces such as
status bars, banners, and log decorators:
tlc.auth.is_activated()— whether 3LC has been activated in this process.tlc.auth.would_authorize()— whether authentication is currently valid (identity intact and not expired).tlc.auth.current_identity()— a frozentlc.auth.Identitysnapshot of the active identity (user_id,tenant_id,auth_mode,provenance), orNone.tlc.auth.expiry_status()—"ok","near_expiry","expired", or"none".tlc.auth.assert_activated()— raise the same error a write would raise right now.
These functions are informational, not enforcement — a write is checked independently when it happens, so an advisory result can be stale by the time a write executes. Use this API to inform users, not to guard a write path.
Static scopes¶
A URL prefix can be declared a static scope — a statement that content at that location will not change. Declare one
with static: true in a config.3lc.yaml beside the data, or with the static flag on a scan-urls entry.
The declaration is enforced, not advisory, and it governs both scanning and writing:
Scanning. A static scope is scanned once and then parked, with no polling timer. This is what makes it worth declaring: a large published dataset costs one scan instead of a scan on every tick.
Writability. Writability checks (such as the
is_url_writableproperty on 3LC objects) answerFalseinside a static scope, without probing storage. A write whose destination 3LC picks — writing a new table without naming its URL, for example — is relocated under the configured root rather than landing in the scope.Direct writes. A write the caller aimed at a location inside a static scope fails with a
PermissionError, since there is nowhere to relocate it to:write_string to <url> is refused: the destination is inside the static scope <scope>, declared by <source>. …
3LC’s own bookkeeping is skipped, not refused. The rule is that a write you aimed at a static scope is refused, while a write 3LC would make for its own record-keeping is skipped — skipping costs at most some recomputation, whereas refusing would break a read that had every right to succeed. Two things fall under it:
index.3lc.jsonchange markers are not written. A parked source never polls, so nothing reads them, and the publisher’s tree stays untouched.tlc.discovery.notify_write()therefore still wakes an indexer in the calling process.Cached table properties — a computed schema,
row_count, a populatedrow_cache_url— are not persisted back to the table’sobject.3lc.json. Reading rows works normally and the row cache is still written to whereverrow_cache_urlpoints; only the note that it exists is not saved. What that costs is below.
When a declaration takes effect¶
The declarations differ in when they start being enforced. A scan-urls flag is in force as the scan URL is added.
So is a config.3lc.yaml sitting at that scan URL’s own root: it is read as part of registering the source, before 3LC
writes anything there — including its own change marker, so a tree published as static stays byte-for-byte untouched. A
config nested deeper in the tree is read by the scan that finds it, and governs from that point rather than from
registration.
A process that never indexes the location does not see the declaration at all: a script that opens a table by URL
without an indexer running gets no protection from a config.3lc.yaml beside that table. Where a location must be
protected regardless of whether it is indexed, declare it consumer-side on the scan-urls entry.
Static means consumers do not see later additions¶
An indexer in another process is parked, with no timer to bring it back. It will not pick up anything added to the
scope after its first scan, and notify_write cannot reach it — the wake is in-process only. So a location you publish
updates to should not be declared static, even though writing to it appears to work locally. A notify into a static
scope warns once per scope for exactly this reason.
On writable storage, static costs recomputation¶
A table’s derived properties — a computed schema, row_count, a populated row_cache_url — are worked out on first
load and persisted back to its object.3lc.json. Inside a static scope that write-back is skipped, so every new
process derives them again. For a table read from a COCO, YOLO or Hugging Face dataset that derivation is the
expensive part of loading, so the declaration trades one-shot scanning for repeated recompute.
On genuinely read-only media the trade costs nothing: the write-back had nowhere to land in any case, and the
declaration only makes that explicit. Weigh it before declaring static on storage that is writable.
Carving out an exception¶
Declarations resolve longest-prefix-wins, so a deeper static: false carves a writable, polled exception out of
a broader static declaration. This is the way to publish a large read-mostly tree while keeping one subtree live:
# <root>/config.3lc.yaml — the whole tree is static
static: true
# <root>/live/config.3lc.yaml — except here
static: false
At the same prefix, the higher configuration tier wins instead, so a user’s own config can override a declaration that shipped with the data.
The root cannot be inside a static scope¶
The root is where writes are relocated to, so a declaration covering it leaves a relocated write nowhere to land. Such a declaration is refused rather than honoured, as soon as it is visible to the process:
At
import tlc, for the root’s ownconfig.3lc.yamland for anyscan-urlsentry whose prefix covers the root. These are the declarations readable from configuration alone, and they are the common case.At Object Service startup, for a
config.3lc.yamlat the root of some other scan URL that turns out to cover the root. Finding it means reading every scan URL’s own config, which the service does when it starts its indexers.At the write itself otherwise — a declaration that only becomes visible mid-run, because a scan discovered a config deeper in the tree or because one was edited after startup. The write is refused with a message naming the scope.
The refusal names the declaring source, since the remedy is to edit that declaration or to move the root, never to change the directory’s permissions.
Troubleshooting¶
**`PermissionError:
The write was aimed at a location declared static (see Static scopes). Nothing was written.
Either point the write somewhere else, carve the location out with static: false in a config.3lc.yaml there,
or stop declaring the scope static.
WARNING: notified a change in the static scope <scope> (declared by <source>); this process will rescan, but indexers in other processes are parked and will not see it
tlc.discovery.notify_write() was called for a location inside a static scope. The in-process indexer was
woken, so the change is visible here — but a static source in another process is parked and will never pick it up. If
consumers elsewhere need to see updates at this location, it should not be declared static.
PermissionError: The 3LC root URL is declared static and cannot be used: <url>.
The configured root falls inside a static scope. 3LC relocates writes to the root, so it must be
writable. Move the root with TLC_PROJECT_ROOT_URL, or carve it out with static: false.
RuntimeError: No active AuthHandle — 3LC is not activated. Log in with 3lc login, or set TLC_API_KEY (or TLC_LICENSE for Enterprise On-Prem) in the environment; the handle is activated on first use.
A write (or tlc.auth.assert_activated()) ran before any activation happened. Activate 3LC as described under
Activation.
ValueError: auth handle expired (mode=<mode>) at <timestamp> — re-activate to continue
The active handle’s lifetime ran out (the timestamp is Unix seconds) and no refresh path could renew it. Re-activate —
for environment-driven activation, restart the process with a valid TLC_API_KEY / TLC_LICENSE.
WARNING: AccountService unreachable since <timestamp> (unix seconds); authentication will fail at <deadline> (unix seconds) unless connectivity is restored
The 3LC account service could not be reached while the handle needed renewal. Writes continue until the stated
deadline; restore connectivity to the account service (or fix the network path to it) before then. A related warning,
auth refresh failing; serving under network-slack window, indicates the same condition on a handle activated in this
process.