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, the TLC_API_KEY environment variable, or the API key file. See API Key for details.

  • License key — provided with the TLC_LICENSE environment 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:

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_writable property on 3LC objects) answer False inside 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.json change 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 populated row_cache_url — are not persisted back to the table’s object.3lc.json. Reading rows works normally and the row cache is still written to wherever row_cache_url points; 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 own config.3lc.yaml and for any scan-urls entry 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.yaml at 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: to is refused: the destination is inside the static scope , declared by

.`**

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.