Configuration reference¶
papio loads TOML from ~/.config/papio/config.toml (on Windows,
%APPDATA%\papio\config.toml) unless the global
--config <path> option selects another file. Configuration is layered over the
built-in defaults; unknown TOML fields are rejected. papio init writes a
validated user-only config file and papio doctor reports readiness.
The tables below list every decoded key in internal/config. Paths beginning
with ~/ are expanded when papio loads them.
Integration credentials¶
papio config credentials manages TypeSafe, OpenAlex, Semantic Scholar, CORE,
Crossref TDM, OpenAIRE, ILLiad and notification-webhook credentials through one
service. The default store is macOS Keychain, Windows Credential Manager or
Linux Secret Service. Linux needs an unlocked Secret Service; Papio never falls
back to a plaintext file when a store is unavailable.
papio config credentials set sources.openalex
papio config credentials status
papio config credentials migrate
set TARGET reads a key at a hidden prompt and creates a fresh record. A secret
manager can pipe input with --key-stdin. Never put a real secret in a command
argument. Use the global --config <path> to select a profile.
The targets are sources.openalex, sources.semanticscholar, sources.core,
sources.crossref_tdm, sources.openaire, agent.typesafe, notify.webhook,
browser.document_delivery, and
browser.resolvers.<name>.document_delivery. Delivery targets exist when that
institution has an ILLiad route configured.
OpenAIRE and webhooks take a typed JSON record through stdin. These examples show the input shape; replace the placeholders through your secret manager, without putting credentials in shell history:
{"version":1,"kind":"webhook","url":"https://hooks.example.org/PRIVATE_ENDPOINT","bearer":"OPTIONAL_BEARER"}
The complete webhook URL is secret storage material: paths and query strings
can contain tokens. Omit bearer when the endpoint needs none. Records are
limited to 2,048 encoded UTF-8 bytes to fit every supported OS store.
Configuration stores an explicit credential_ref, such as
keyring:0123456789abcdef0123456789abcdef, at the consuming integration.
Notifications use notify.webhook_credential_ref. Moving the config or data
directory preserves this binding. Copying config to another machine does not
copy its secrets. To share a record deliberately, use
papio config credentials bind TARGET REFERENCE. The record's kind must match
the target. Binding TypeSafe also enables cloud decisions for that profile;
a key stored for another profile does not enable them.
For headless use, bind env:YOUR_SECRET_VARIABLE. Single-key integrations read
the variable as a key; OpenAIRE and webhooks read the JSON shapes above. The
variable must be available when binding and when starting the daemon. Papio
snapshots selected variables and removes them before launching vault helpers,
PDF workers or hooks. References are authoritative: a missing or invalid value
never falls back to another credential.
status [TARGET] reports the selected source and whether it resolves locally;
it does not test provider authentication. detach TARGET removes the binding
without deleting a shared record. delete REFERENCE explicitly deletes an OS
record and can affect other configs that refer to it. Restart the daemon after
changes with papio daemon stop; the next ordinary command starts it again.
migrate converts this profile's legacy TOML secrets and old TypeSafe record.
It validates the inventory, creates and verifies fresh records, rejects observed
concurrent config edits, then publishes one atomic config update. A failed save
leaves the original config in place and reports any staged references for cleanup.
It leaves the old TypeSafe record intact and saves the previous config to
config.toml.bak (mode 0600), which can still hold the literal secrets; delete it
after you check the result. An active legacy TypeSafe environment override must be
removed before migration.
Install a compatible daemon and native host first: older strict config parsers
cannot read the new reference fields.
Resolved secrets stay in runtime memory, separate from serializable config.
Missing keys disable required-key integrations; services with a supported
keyless mode continue at that mode's quota. papio doctor explains the selected
credential's state. Unrelated acquisition remains available.
Agent acquisition¶
Run papio config agent set to enable TypeSafe/Jev for delegated browser
acquisition when packaged and generic routes fail. It reads your key at a hidden
prompt and saves it in macOS Keychain, Windows Credential Manager or Linux
Secret Service. A secret manager can pipe the key into
papio config agent set --key-stdin; avoid literal keys in shell commands.
These commands are compatibility entry points to the shared credential service
above. New setup writes an explicit reference, so moving the profile preserves
its key binding. Existing path-derived records remain readable until migration.
On Windows, run setup and start the daemon from your signed-in desktop session.
SSH network logons cannot read Windows Credential Manager's user credential set;
a daemon started there cannot use the saved key. This is a
Windows logon-session constraint,
not a missing key. Use a local terminal or let the browser's native host start
the daemon.
Supplying the key opts into sending the article DOI, bounded title and sanitized
control descriptions to TypeSafe; see Privacy.
The credential never enters TOML, the extension or job history. Setup stores
backend = "typesafe" and a non-secret credential_ref in the [agent] section. Empty or absent backend disables
stored-key lookup; other backend values are currently rejected. This new section
requires a compatible daemon and native host before it is added to an older
installation's config.
Run papio daemon stop after setup; the next ordinary command starts the daemon
with the new setting. papio config agent status reports saved configuration,
credential availability and the current command's environment override, without
printing a key or claiming the running daemon reloaded. papio config agent remove
detaches the key and disables enrollment without deleting the stored record;
restart afterward. Use papio config credentials delete REFERENCE --yes for explicit
record deletion; without --yes it only describes the deletion, and it refuses a
record this configuration still binds. If the OS store
is unavailable, agent inference is disabled with a daemon diagnostic, while
ordinary acquisition remains available.
PAPIO_TYPESAFE_API_KEY in the daemon's environment remains a legacy override
when no agent.credential_ref is configured, and does not require [agent].
An explicitly empty legacy value disables Jev even when an old saved key exists.
A new explicit reference takes precedence over this legacy variable. Removing the variable restores the profile's configured behavior
after restart. To disable Jev completely, remove both enrollment and any launch
environment override. Changing your shell's environment does not change an
already running daemon.
Both daemon and extension must support agent_fallback_v1. Effective site access
is still required, including for publishers without an adapter. The initial
implementation handles controls on a DOI-identified article in a managed tab.
Chrome uses its filename-steering download API. Firefox requires both peers to
support native_click_adoption_v1. It uses a fresh download with an exact article
referrer, then asks the daemon to copy and validate that file. The file must be
directly in the parent of the configured adoption root, normally Downloads.
Custom browser download directories must match that configuration. Missing
referrers, ambiguous downloads, UNC paths and save dialogs remain assisted.
With agent_navigation_v1, both browsers can follow a selected same-origin link
in the same tab, verify the destination DOI and continue under the original
attempt. Firefox transfers its unused reservation without resetting its expiry
or download-directory baseline. Unexpected redirects, new tabs and native PDF
viewer saving still require assistance.
An HTML PDF wrapper can be followed when it exposes one matching file. This is
distinct from saving a document already loaded in the native PDF viewer; signed
viewer URLs are not replayed to simulate that capability.
The daemon and decision contract are shared across operating systems.
It allows up to 60 decisions in ten minutes per drive permit and 30 seconds per
model call. Failures consume their call reservation; retries do not reset the
budget. These limits are implementation defaults, not TOML settings.
The daemon removes the key from its process environment after reading it, before starting PDF workers, hooks or other integrations. The backend interface supports local implementations without a key or network; no local model runtime ships yet. Credential setup is currently a CLI command.
Top-level keys¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
access_mode |
string | empty | Required before acquisition. Allowed values are conservative, assisted, and delegated; a fresh guided papio init chooses conservative. Conservative records institutional OpenURL availability without opening a handoff; assisted and delegated can route eligible exhaustion to browser handoff. |
email |
string | empty | Contact identity for polite API pools. Sent to third parties: as a query parameter to Unpaywall (required) and OpenAlex (required), to Crossref metadata and Retraction Watch when set, in the User-Agent of DOI-registration lookups when set, and in the User-Agent of open_books repository lookups (IRIS by default; OAPEN when open_books.oapen is on). See Privacy for the full recipient table. Doctor fails when an enabled source that needs one (Unpaywall, OpenAlex) has no email; OpenAlex without an API key runs on the smaller anonymous allowance instead, and doctor warns. |
data_dir |
path string | ~/.local/share/papio (Windows: %LOCALAPPDATA%\papio) |
Private writable data directory for the database, artifacts, and socket. Browser-download adoption does not follow this directory: the adoption root defaults to <your download folder>/papio (see [browser] download_adoption_root), while the historical <data_dir>/adoptions path remains drain-only for files already sitting there. |
[fetch]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
max_bytes |
integer bytes | 104857600 (100 MiB) |
Maximum artifact-download size, and the largest file papio jobs supply-pdf accepts. It must be at least 1048576 (1 MiB). |
timeout_seconds |
integer seconds | 120 |
Fetch deadline. It must be in 5..3600 seconds. |
allow_http_loopback |
boolean | false |
Development and test override that permits HTTP loopback. Doctor warns while it is enabled; production policy is HTTPS-only. |
[host_suppression]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
enabled |
boolean | true |
Pace the daemon away from publisher hosts that answer with access-control walls instead of files (challenge/403). Suppression skips the daemon fetch, never the candidate: the candidate still reaches the browser or institutional route. |
failure_threshold |
integer | 3 |
Kind-matching daemon failures (challenge + 403 pooled) with zero daemon fetch successes for the host inside the window before suppression starts. It must be between 1 and 100. |
window_days |
integer days | 90 |
Trailing window for the failure and success counts. It must be between 1 and 365. |
probe_interval_days |
integer days | 7 |
At most one real revival probe per suppressed host per interval. It must be between 1 and 90. |
rate_limit_backoff_cap_seconds |
integer seconds | 3600 |
Cap for the per-host 429 backoff honoring Retry-After. 429s back off, never suppress. It must be between 60 and 86400. |
min_wall_span_hours |
integer hours | 24 |
The walls inside the window must span at least this long before a host suppresses, so one burst of refusals from an intermittent host never does. 0 disables the check. It must be between 0 and 720. |
excluded_hosts |
list of hostnames | ["doi.org"] |
Hosts that never suppress (pure redirectors). Each entry must be a bare hostname. |
The [host_suppression] section is strict-mode configuration: an older daemon rejects a
config containing it. Deploy the binary that supports this section together with
the configuration change.
[captures]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
enabled |
boolean | true |
Stores sanitized diagnostic page captures received from the extension in the local data directory. Disable it to keep no diagnostic page HTML. |
max_per_host |
integer | 10 |
Maximum retained captures for each host. It must be between 1 and 1000; when the limit is exceeded, the oldest captures for that host are removed. |
max_age_days |
integer days | 14 |
Maximum capture age. It must be between 1 and 365; older captures are removed. |
The [captures] section is strict-mode configuration: an older daemon rejects a
config containing it. Deploy the binary that supports this section together with
the configuration change.
[pdf]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
ocr_enabled |
boolean | true |
Enables the OCR fallback. If it is enabled, doctor requires both pdftoppm and tesseract; disabling it makes image-only papers require review. |
min_text_chars |
integer | 400 |
Minimum extracted-text threshold used by PDF validation before OCR fallback is relevant. |
max_ocr_pages |
integer | 4 |
Maximum pages processed by the OCR fallback. |
title_match_threshold |
number | 0.6 |
PDF title-match threshold. It must be greater than 0 and no greater than 1. |
[browser]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
extension_id |
string | empty | The Chrome extension ID allowed to use the native host. It must be 32 characters from a through p; an empty value disables the bridge. |
extension_ids |
list of strings | empty | Additional Chrome-family extension IDs allowed to reach the native host alongside extension_id — e.g. an Edge Add-ons store copy or a second keyed build, which carry different IDs than the Chrome Web Store package. Each is 32 chars a–p. The manifest's allowed_origins lists extension_id plus every entry here. |
firefox_extension_id |
string | empty | The Firefox (Gecko) add-on ID allowed to use the native host — papio@orgmentem.com for the built extension. Accepts an email-style ID or a braced GUID; an empty value disables the Firefox bridge. |
direct_routes_enabled |
boolean | true |
Enables papio's compiled-in provider-direct HTTPS route candidates when the connected extension advertises support. Set false to disable these routes daemon-wide; institutional OpenURL routing is unchanged. |
openurl_base_url |
string URL | empty | Legacy/default institutional OpenURL resolver base. It must use https:// and must not contain a # fragment or userinfo (named profiles follow the same rule); an empty value prevents default-profile institutional routing. Existing query parameters are preserved when papio adds citation metadata. Prefer the institution's direct-link-enabled endpoint so a single electronic service bypasses the resolver menu. |
shibboleth_entity_id |
string URL | empty | Default institution's Shibboleth IdP entityID (https://). When set, a provider login wall is auto-routed to this IdP (skipping the WAYF selector). Empty disables federated login-routing for the default profile. |
proquest_account_id |
string digits | empty | Default institution's ProQuest account id (digits, max 64). When set, papio appends ?accountid=<id> to unlock the institution's ProQuest link-resolver without a manual sign-in. Empty disables the append. During papio init you may paste a ProQuest URL containing accountid= instead of the bare id. |
openathens_domain |
string domain | empty | Default institution's OpenAthens organisation domain: the <domain> in go.openathens.net/redirector/<domain>, such as example.edu. It must be a bare lowercase DNS name of at least two labels (letters, digits, hyphens and dots; no scheme, slash, port or query) and is rejected at load otherwise. When set, papio sends the DOI browser route through https://go.openathens.net/redirector/<domain>?url=https://doi.org/<doi>. That redirector signs in to the publisher with your institution's identity provider and then opens the article, so a paper whose publisher has a packaged adapter reaches its page without the library's discovery layer. papio offers it first and falls back to the openurl_base_url route when it parks without a PDF. Empty disables it. Built only from this value and the job's validated DOI — never from a page. Because config is strict-mode, deploy the binary that knows this key before you add it. See Use your library's OpenAthens link. |
libkey_mode |
string | empty | LibKey routing for the default institution: empty or off disables it; link routes DOI/PMID handoffs through the keyless libkey.io/libraries/<id>/<doi-or-pmid> institution link ahead of the bare OpenURL resolver, falling back to OpenURL for works without a DOI or PMID and whenever LibKey is unavailable. api is reserved and rejected (not implemented). link requires both a positive libkey_library_id and a non-empty openurl_base_url on the default institution. |
libkey_library_id |
integer | 0 |
The institution's numeric Third Iron library id — the number in its BrowZine/LibKey.io URL (…/libraries/<id>/…). Required (positive) when libkey_mode = "link"; setting it without link mode is rejected rather than left silently dead. |
default_resolver |
string | empty | Named [browser.resolvers.<name>] profile used when a request omits resolver (e.g. papio acquire without --resolver). Empty preserves the historical default institution. Must name a configured profile — the implicit default profile (when openurl_base_url is set) or a [browser.resolvers.<name>] key; an unconfigured name is rejected at load. |
download_adoption_root |
path string | empty | Root for browser-download adoption; adoption is confined to a job subdirectory beneath it. When empty the effective value is <your download folder>/papio — on macOS ~/Downloads/papio, on Linux/BSD the XDG XDG_DOWNLOAD_DIR folder (falling back to ~/Downloads), on Windows the Downloads known folder. It must be a papio directory inside the folder your browser downloads into: Chrome can only steer a download to a path relative to its own download directory, so any other root adopts nothing at all, silently. papio init creates the effective root, and papio doctor's adoption_root check fails and names the resolved path when it is unreachable. |
action_expiry_seconds |
integer seconds | 1800 |
Browser-handoff expiry and the initial age before an open human action is reminded. Later reminders double their per-action interval through 24 hours. It must be in 0..2592000 seconds (30 days). |
native_viewer_helper |
removed | — | Removed. It named the macOS helper program that saved a PDF from Firefox's viewer; the extension now saves that PDF itself. A config that still sets it loads, the value is ignored, and papio doctor warns with config_ignored_settings. Delete the line. |
papio init can derive openurl_base_url from a pasted library discovery URL
(--institution-url) or from the resolver configured in Zotero.
The browser path uses the user's ordinary Chrome session. It is not configured with passwords, MFA, CAPTCHA tokens, or publisher credentials.
papio init collects extension_id and firefox_extension_id during setup
(Firefox defaults to the built add-on's papio@orgmentem.com), or set them
non-interactively with --extension-id / --firefox-extension-id, so the
native messaging host installs on the first run.
[browser.resolvers]¶
Named resolver profiles are per-institution tables keyed by a lowercase
alphanumeric name; default is reserved for the implicit top-level
institution and is rejected at load if used as a profile key. A name that matches a packaged provider route hint (a provider family such as sage-doi-pdf, or an exact route revision) is also rejected at load. Each carries
its own OpenURL base and, optionally, the same shibboleth_entity_id,
proquest_account_id, openathens_domain, libkey_mode, and libkey_library_id fields as the
default [browser] institution — so a multi-institution user routes each
job's login to the right library instead of inheriting the default's
identity:
[browser.resolvers.campus]
openurl_base_url = "https://library.example.edu/discovery/openurl?institution=EXAMPLE"
shibboleth_entity_id = "https://idp.example.edu/idp/shibboleth" # optional
proquest_account_id = "12345" # optional
openathens_domain = "example.edu" # optional
libkey_mode = "link" # optional
libkey_library_id = 1234 # required for link mode
A profile may also be written as a bare string — campus =
"https://library.example.edu/discovery/openurl?institution=EXAMPLE" — which is
shorthand for a table with only openurl_base_url set. This keeps older
single-base configs valid; add the table form when a profile needs its own
login identity.
Select one with papio acquire --resolver campus, papio acquire --batch
works.json --resolver campus, or the corresponding MCP field. The selected
name is snapshotted in the job policy, so re-opened actions cannot silently
fall back to another institution.
Set browser.default_resolver to one of these names (or leave it empty) to
choose which profile an omitted resolver uses; an explicit
--resolver/MCP resolver value always takes precedence, and an
unconfigured default_resolver value is rejected at load.
On a tracked Alma/Primo resolver page, the extension may follow the first
same-origin resolveService link selected by the institution's Online Services
order. This emulates resolver direct linking without accepting provider terms
or initiating physical-item, scan, or interlibrary-loan requests. Script access
remains constrained by extension/manifest.json host permissions; an unlisted
custom resolver origin stays in assisted mode.
[browser.document_delivery]¶
Configures the default institution's document-delivery / interlibrary-loan
route (ADR-0017). Omitting the table disables it — a job that exhausts every
acquisition candidate falls back to the profile's plain OpenURL route, the
same behavior as before this table existed. The identical table nests under
[browser.resolvers.<name>.document_delivery] for a named institution
profile, alongside that profile's own openurl_base_url,
shibboleth_entity_id, proquest_account_id, openathens_domain, libkey_mode, and
libkey_library_id.
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
kind |
string | none (required) | Delivery adapter: openurl | libkey | illiad | custom. Required — papio never guesses which ILL system an institution runs from branding or a landing page. oclc and rapido are named as intended future providers but rejected: a kind whose adapter has not shipped must not parse, the same fail-closed rule sources apply to [sources.<name>]. |
base_url |
string URL | empty | Request form or API base. Used as the request-form URL for openurl/custom, and as the ILLiad Web Platform base for illiad. Must use https://. |
patron_web_base_url |
string URL | empty | Patron-facing ILLiad web client base (the illiad.dll URL patrons use), permitted only for kind = "illiad". Must use https://. Never derived from base_url — shared-server sites and customised directories make derivation unreliable. When set, a fulfilled request's "View PDF" page (form 75) routes through the ordinary browser handoff for retrieval; when empty, fulfillment falls back to a recorded human action and the compiled profile reports fulfillment: none even if submission is auto-capable. |
allowed_hosts |
list of strings | empty | Hosts a prefilled request form or API base may reach. |
submit_policy |
string | never |
never (default) | prefill_only | auto_if_unconditional. Narrows what the daemon-wide access_mode permits — it never widens it: under conservative the route is discovered and recorded only, never opened or submitted; under assisted the prefilled form opens but submission stays human. auto_if_unconditional is accepted only when kind = "illiad": openurl, libkey, and custom route to a form with no deterministic submission-and-reconciliation contract, so they compile permanently prefill_only. |
request_classes |
list of strings | empty | Request classes this profile is declared for. v1 recognizes only digital_journal_article; any other value is rejected as not yet modelled. |
legal_basis |
string | unknown |
institution_policy | copyright_act_s49 | unknown. Configured, never inferred from a hostname. copyright_act_s49 (Australian document supply) compiles prefill_only permanently, by statute: the patron's declaration is an affirmative, per-request statutory act — "not previously supplied" — that no standing declaration can truthfully cover, and papio must never tick, script, or represent it. An AU-jurisdiction profile defaults patron_attestation to unknown until the institution confirms otherwise. |
patron_attestation |
string | unknown |
not_required | standing_completed | per_request | unknown. standing_completed counts only when the institution has confirmed a registration-time agreement covers API-created requests of this class — never inferred from an account's existence, a missing checkbox in one render, an API accepting a request, or the institution's country or hostname. |
patron_fee_policy |
string | unknown |
zero_standard | per_request | unknown. Only zero_standard can ever compile auto_capable — v1 auto-submission covers zero-patron-fee digital journal articles only; books, chapters, theses, physical loans, rush service, and any nonzero or provider-quoted fee stay prefill_only. |
monthly_request_cap |
integer | 0 (no cap) |
Bounds auto-submitted requests per calendar month; 0 means no declared cap. Values below zero are tolerated and behave as 0 (no cap). |
status_poll_minutes |
integer minutes | 0 (adapter default) |
Delivery status poll cadence; 0 uses the delivery service's own default. Values below zero are tolerated and behave as 0 (the default). Delivery polling draws on its own budget, never on ordinary resolver/HTTP retry counts, so a slow ILL turnaround cannot exhaust the acquisition waterfall's retry budget. |
credential_ref |
string | empty | OS-store or environment reference for this institution's ILLiad key. Set it with papio config credentials set browser.document_delivery (or the named resolver target). Cannot coexist with api_key. |
api_key |
string | empty | Legacy ILLiad application key, supported for compatibility. Migrate it into the credential service. Never sent to the extension or browser wire; rejected for other delivery kinds. |
patron_ref |
string | empty | Configured, non-secret patron reference used to map requests to the institution's system. Personal identity data, not a secret: 0600 config only, redacted from events, diagnostics, and delivery provenance. |
A compiled auto_capable verdict additionally requires one recorded live
acceptance — a supervised submit-and-reconcile against the real deployment
under the institution's authority — so a compiled adapter plus matching
config is necessary but not sufficient. papio init prints the compiled
gate class before saving (AUTO-CAPABLE with its evidence, or
PREFILL ONLY with the specific blocker) and papio doctor verifies what
is verifiable while keeping DECLARED configuration and PASS/OBSERVED
facts strictly separate: neither command claims automatic submission for a
profile that cannot actually reach it.
[browser.resolvers.campus.document_delivery]
kind = "illiad"
base_url = "https://ill.campus.example.edu/illiadwebplatform"
allowed_hosts = ["ill.campus.example.edu"]
submit_policy = "auto_if_unconditional" # or "prefill_only" / "never"
request_classes = ["digital_journal_article"]
legal_basis = "institution_policy" # or "copyright_act_s49"
patron_attestation = "standing_completed" # or "not_required" / "per_request"
patron_fee_policy = "zero_standard"
monthly_request_cap = 25
status_poll_minutes = 60
credential_ref = "keyring:0123456789abcdef0123456789abcdef"
patron_ref = "configured-non-secret-reference"
[retraction]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
scope |
string | acquired |
Selects the DOI corpus for the daily Retraction Watch check. acquired checks works that papio has acquired. library also checks DOI records from every [[library.sources]] export and, when configured, the personal Zotero library through paged zotio --agent items list reads. Allowed values are acquired and library. The check downloads the Crossref Retraction Watch CSV at most once daily, caches its last valid copy in data_dir, and matches locally. Thus its request count does not grow with the library size. The CSV response limit is 256 MiB; the live dataset was 66.7 MB with 72,526 rows on 2026-09-15. |
The default preserves the earlier job-scoped behavior. An upgrade does not silently disclose a wider DOI corpus. This strict-mode section requires a daemon version that recognizes it.
An empty or sharply reduced library export can indicate an unfinished write. The sweep keeps earlier notices and acknowledgements while it confirms the smaller size. Consecutive complete reads must report the same smaller DOI count over at least 72 hours before the sweep accepts it. A restored export, a different count, or an enumeration failure resets that window. The cache keeps the confirmation state across daemon restarts.
[zotio]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
executable |
path or command string | zotio |
zotio executable papio invokes at the Zotero boundary. Optional: an empty value disables the deep Zotero integration (auto-import, plan/apply, queue). When no generic library.sources authority is configured, ownership lookup then classifies every work as not-owned. Required only when auto_import = true. |
timeout_seconds |
integer seconds | 120 |
zotio command deadline. It must be between 5 and 600 seconds inclusive. |
attachment_mode |
string | stored |
zotio attachment mode. Allowed values are stored and linked-file. |
auto_import |
boolean | false |
Default acquisition policy for automatic zotio plan-and-apply after a job is ready. An acquire --auto-import request can opt in per job. |
auto_import_paused |
boolean | false |
Pauses all automatic Zotero imports, including retries and jobs that already saved auto_import = true. Acquisition and library lookups continue; job policies stay unchanged. This also pauses the follow-up retries after an import: a collection filing or abstract fill the Web API refused with 404 is repeated up to four times, and these pause too. Restart the daemon after changing this setting. Set it back to false to resume eligible pending imports. Explicit manual plan/apply commands are unaffected. |
auto_enrich |
boolean | true |
Fills a missing DOI and abstract on an auto-imported paper. In stored mode, a new paper whose Crossref record has no abstract gets the discovery source's abstract (OpenAlex by default) before Zotero desktop saves it. Any field still missing after the import gets the conservative scoped zotio enrichment of the imported parent. |
exception_tags |
boolean | false |
Enables the reconciled exception-tag ledger: papio maintains papio:needs-action and papio:unavailable as Zotero automatic tags on provenance-confirmed personal-library items, reconciling job state with current attachment state (papio zotio tags reconcile runs one pass on demand). Requires executable and zotio ≥ 0.13.0. Lifecycle states are never tagged; a same-name manual tag is never retyped or removed. After a daemon reload, turning this off makes the next pass remove papio-owned tags. |
retraction_tags |
boolean | false |
Enables the papio:retracted automatic tag on personal-library items whose DOI exactly equals the DOI of a current verified retraction notice (the retraction sentinel's cache; [sources.retraction_watch] must be enabled, and retraction.scope decides which works get notices at all). Corrections and expressions of concern are never tagged. papio zotio tags retractions previews the changes and --apply writes them; with the option on, the daemon also reconciles after the notice set changes and at most once a day. Acknowledging the inbox notice does not remove the tag. A withdrawn notice, or turning the option off, removes only the tag papio owns; a same-name manual tag is never retyped or removed. Requires executable and zotio ≥ 0.13.0. |
unavailable_recheck_days |
integer days | 14 |
How long an unavailable outcome parks an item before Zotero backfill or the source-independent daemon runner re-checks it through the ordinary submission path (open-access availability drifts upward). Must be between 1 and 365 inclusive. |
papio invokes zotio but does not read or store Zotero credentials. Item and
attachment mutation stays preview-first: papio zotio plan returns immutable
plans and papio zotio apply requires the exact confirmation SHA-256.
papio zotio tags reconcile is the separate exception: it converges papio's
own papio:needs-action and papio:unavailable tags with no preview step.
papio zotio tags retractions previews by default and writes only with --apply.
[hooks]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
on_ready |
shell command string | empty | When set, runs once via the system shell (/bin/sh -c; on Windows cmd /d /s /c "<command>", which runs the command exactly as written, quotes included) each time a job reaches ready (validated artifact). Job metadata arrives as PAPIO_* environment variables. Fire-and-forget: a failing hook is recorded as a hook.on_ready job event but never fails or retries the job. Empty disables it. See the hooks guide. |
timeout_seconds |
integer seconds | 120 |
Deadline for one hook run. Validated (5..600) only when on_ready is set. |
[filing]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
folder |
string | empty | Opt-in managed folder destination. Requires an absolute path without surrounding whitespace. Empty disables it. The daemon keeps destination-specific receipts and retries safe folder writes. Arbitrary hooks.on_ready scripts remain best-effort and do not gain automatic retries. |
See the filing guide for receipt inspection and explicit retry commands.
[[library.sources]]¶
Libraries papio consults to answer "do I already hold this paper?" for users
who do not run Zotero. Repeat the table for each source (maximum 8 — every one is
read on every search, batch, and discovery watch pass). Generic
library.sources are ignored while zotio.executable is configured; otherwise
they are the ownership authority for discovery watches: acquire watches, alert
watches, and papio acquire --from-digest.
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
name |
string | — | Required, unique, and must have no leading or trailing whitespace. Identifies the source in papio doctor output and in warnings. |
kind |
string | — | Required. file reads a bibliographic export; command runs a program and reads its standard output as the export. Anything else is rejected rather than ignored. |
path |
string | — | Required for kind = "file", and must be empty for kind = "command". The bibliographic export to read. ~ is expanded and the resulting path must be absolute. |
format |
string | empty | bibtex, ris, csl-json, or nbib. Empty detects from the path (file sources) and content. |
claim |
string | — | Required, no default: pdf_present (entries whose full text you hold, so a match may skip acquisition) or record_present (citations only — annotates papio search but never skips). |
argv |
array of strings | — | Required for kind = "command", rejected for kind = "file". The program and its arguments, run directly with no shell. The first element must be an absolute path; a leading ~/ expands when the command runs. papio never looks the program up on PATH, so a bare name such as papis or a relative path such as ./export is rejected when the config loads. The daemon's PATH is often not your shell's, so a PATH lookup could fail in the daemon, or run a different program there, after papio doctor accepted it. A program that is missing when the command runs fails the read, and papio doctor reports that the command could not be started. A shell string is not accepted: to use a pipeline, name the shell, for example ["/bin/sh", "-c", "papis export --all --format bibtex"]. The shell then finds papis on the daemon's PATH. |
timeout_seconds |
integer seconds | 30 |
kind = "command" only. Deadline for one run, 1 to 300. At the deadline papio stops the command and all the processes it started, and the read fails. |
max_output_bytes |
integer bytes | 33554432 (32 MiB) |
kind = "command" only. Largest standard output accepted, 1 to 134217728 (128 MiB). More output fails the read; papio never indexes a cut-off export. |
refresh_seconds |
integer seconds | 300 |
kind = "command" only. How long one successful run answers lookups before the command runs again, 10 to 86400. |
A command source runs at most once per refresh_seconds, never once per
paper. Lookups that arrive while it runs share that one run. The command
inherits the daemon's environment. Its standard input is empty, and papio
never reads or reports its standard error. When the command exits, papio
stops every process it started and left running, so do not start background
work from it. The command must print only the
records its claim covers. For pdf_present, print only the entries whose PDF
you hold; papio does not filter the output by attachment fields. A run fails
when the command exits non-zero, passes timeout_seconds, prints more than
max_output_bytes, or prints output that does not parse. A failed run never
counts as an empty library: the source reports as unreadable. The previous
successful run can then still mark papio search results, but it never skips an
acquisition. After a failure, papio waits up to 30 seconds (or
refresh_seconds, if shorter) before it runs the command again. When the last
successful run is older than refresh_seconds plus 15 minutes, its results are
no longer shown at all.
Both file and command sources keep their last good entry count in data_dir.
A drop below one tenth of a previous count of at least 20 entries makes the
source incomplete. This protects against truncated exports even after a
restart. An intentional reduction becomes complete after consecutive successful
reads report the same smaller count over at least 72 hours. A restored export,
a different count, or a failed read resets that confirmation window.
Matching is exact on identifiers represented by the source format. No format
supports every identifier, and titles are never matched. ISBN is excluded,
because an edited volume shares one ISBN with every chapter in it. papio never
infers PDF presence from per-manager attachment fields (BibTeX file, papis
files) — the source declares it via claim.
| Format | DOI | arXiv | PMID |
|---|---|---|---|
| BibTeX | yes | yes | yes |
| CSL-JSON | yes | no | yes |
| NBIB | yes | no | yes |
| RIS | yes | no | no |
A source unreadable to papio is reported as unreadable, not as holding
nothing: papio acquire --batch then refuses to create jobs rather than
re-downloading the whole batch. Before the fifth consecutive failure, each
cadence attempts another run; a successful run resets the failure count. The
fifth consecutive failure disables the watch. After fixing the source, force a
run with papio watch run <id>; when it succeeds, papio watch resume <id>
puts the same watch back on its schedule.
--include-owned is available only for papio acquire --batch, meaning
"proceed despite ownership uncertainty". Because a bibliographic export cannot
say which manifestation it holds, such a source never satisfies an explicit
--desired-version published request. papio doctor performs a fresh one-shot
probe of each source and reports that read's record count and outcome; it does
not report daemon cached age, count-collapse detection, or retained failure
state. See the filing guide.
[notify]¶
The notification router has two independent legs: local desktop interruptions
for a human, and webhook delivery for automation. Desktop delivery is
best-effort and currently available only through the macOS sender; an
unsupported platform is reported by papio doctor and does not consume the
desktop rate budget. Webhooks are not subject to quiet hours, focused-surface
suppression, or the desktop rate ceiling.
The daemon claims a webhook before it sends the POST. It does not send that
intent again after a crash or a failed response. A later event that arrives
after the claim gets a new delivery_key and its own POST. Activity attempts
to record a failed attempt, but a logging failure can leave no Activity row.
The JSON event carries the key so receivers can check each delivery.
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
enabled |
boolean | true |
Enables best-effort local desktop notifications from the daemon. This affects only the desktop leg; it does not disable webhooks. |
preset |
string | milestones |
Base desktop/webhook routing policy. Accepted values are quiet, milestones, and verbose; per-category overrides below layer on top. |
max_per_hour |
integer | 6 |
Maximum desktop notification reservations in a rolling hour. 0 means unlimited. Must be in 0..10000. This ceiling never applies to webhooks. |
quiet_hours |
string | empty (disabled) | Optional local civil-time interval in HH:MM-HH:MM form. The start and end must differ; spring-forward and fall-back transitions are handled as one local interval. |
quiet_mode |
string | hold |
Behavior for desktop notices during quiet_hours: hold keeps them pending until the window ends; drop records them as dropped. Accepted values are hold and drop. |
digest_every_minutes |
integer minutes | 240 |
Interval for pending-decision digests. 0 uses the four-hour default. Must be in 0..10080. |
completion_quiet_minutes |
integer minutes | 10 |
Quiet period before a batch completion checkpoint may be surfaced. Must be in 0..1440. |
completion_max_hold_minutes |
integer minutes | 120 |
Maximum hold for a batch completion checkpoint. Must be in 0..10080; when nonzero, it must be at least completion_quiet_minutes. |
stall_after_minutes |
integer minutes | 30 |
Minimum named episode age before a system-degraded/stall notice. Must be in 0..10080. A scheduled retry or delivery poll is not a stall. |
categories |
table of tables | empty | Per-category overrides. Keys must be one of the seven names listed below; an unknown category is rejected. |
webhook_url |
string URL | empty | When set, notification intents are delivered as POSTs to this automation endpoint, in the shape set by webhook_format (JSON by default). It is independent of enabled and desktop policy; must be an absolute https URL (plain http is accepted only for a loopback host: localhost, 127.0.0.1, or [::1]). A webhook credential record follows the same rule. papio does not follow redirects from the endpoint; a 3xx answer counts as a failed delivery. |
webhook_credential_ref |
string | empty | Reference to a webhook record containing the complete endpoint URL and optional bearer. Set with papio config credentials set notify.webhook --key-stdin. Cannot coexist with webhook_url or webhook_secret. |
webhook_secret |
string | empty | Legacy bearer token for webhook_url. Use papio config credentials migrate to move both into one record. |
webhook_format |
string | json |
Request shape for the webhook, whether it comes from webhook_url or a webhook credential record. Accepted values are json, ntfy, and slack; any other value is rejected. json posts {source, event, message, count, …, sent_at} with Content-Type: application/json. ntfy posts message as a plain-text body with Title: papio[: <event>][ (<count>)], Tags: papio[,<event>], and Priority (high for action.opened and action.reminder, default otherwise); a configured bearer is sent as Authorization: Bearer, which ntfy accepts. slack posts {"text": "*<title>*\n<message>"}, the Slack incoming-webhook shape, with &, <, and > escaped. ntfy and slack also send the stable delivery key in an X-Papio-Delivery-Key header, because their bodies have no field for it. An empty value means json. |
The milestones preset is the default: it sends immediate coalesced
standalone outcomes and newly opened decisions, a four-hour pending-decision
digest, a bounded batch checkpoint/final summary, discovery as catch-up or
digest material, capped integrity notices, and one system-degraded notice per
episode. Work that is continuing or scheduled does not produce a desktop
interruption. The preset treatment is:
| Category | quiet |
milestones (default) |
verbose |
|---|---|---|---|
request_outcome |
Immediate failures; digest successes | Immediate, coalesced | Immediate |
decision_opened |
Digest | Immediate, coalesced over 5 minutes | Immediate, coalesced over 60 seconds |
decision_pending |
Off | Digest, at most every 4 hours | Immediate, once per pass |
completion_batch |
Off | One checkpoint when useful plus one meaningful final delta | One checkpoint when useful plus one meaningful final delta |
discovery_new |
Off | Catch-up or desktop digest | Immediate, coalesced |
integrity_notice |
Immediate, capped | Immediate, capped | Immediate, capped |
system_degraded |
Immediate, state-deduped | Immediate, state-deduped | Immediate, state-deduped |
[notify.categories.<name>]¶
Use one table for each category whose routing should differ from the preset:
[notify.categories.decision_opened]
desktop = "immediate" # off | digest | immediate
webhook = "immediate" # off | digest | immediate
window_seconds = 300
The valid category names are request_outcome, decision_opened,
decision_pending, completion_batch, discovery_new, integrity_notice,
and system_degraded. desktop and webhook each accept off, digest, or
immediate; an empty value inherits the preset. window_seconds defaults to
the preset's window when 0 (or when omitted) and otherwise must be in
0..86400. Webhook mode is independent automation routing: it remains
immediate by default and ignores desktop quiet hours, focus suppression, and
rate limits. A category may override its webhook mode without changing its
desktop mode.
This section is strict-mode configuration. Deploy the binary that understands
these fields together with the configuration change; an older daemon rejects
unknown [notify] fields.
[updates]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
check |
boolean | false |
Enables the daemon's once-daily background check for a newer papio or zotio release against GitHub's public release API. Sends no identifier, count, or telemetry beyond the anonymous request GitHub receives for any web hit. See Privacy. papio init's guided setup suggests enabling it (non-interactive --check-updates also defaults true); the config default when the key is absent (or on a config written before this key existed) is false. papio doctor reports the check as skipped while it is off. The same switch controls the once-daily fetch of the signed adapter control document (control_url below); with it off, the daemon makes no control request but keeps applying any control it already holds. |
control_url |
string | empty (= https://raw.githubusercontent.com/OrgMentem/papio/main/control/adapter-control.json) |
Where the daemon fetches the signed adapter control document when check is on. The document can only suspend exact packaged adapter revisions and safety domains, so a broken adapter stops automating without a store release; a higher sequence lifts it. Fetched at most once a day, on the same cadence as the release check. A 404 means nothing is published and is normal. The daemon verifies an Ed25519 signature against the papio control key embedded in the binary and refuses unsigned, tampered, replayed, or older documents, so the URL only has to be reachable, not trusted. Must be https, or http to a loopback host for a local test server. A build that embeds no control key fetches nothing. papio doctor reports the adapter control state and every suspension. See Privacy. |
[discovery]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
sources |
string array | empty (= ["openalex"]) |
Discovery backends for papio search and watches, in merge-preference order. Valid entries: arxiv, openalex, semanticscholar (each at most once). Results merge with DOI-then-title deduplication; earlier backends win ties. arXiv supports text and submitted-year searches but not citation snowballs. Per-backend credentials bind through sources.<name>.credential_ref (optional for Semantic Scholar, which works keyless at public rate limits); arXiv needs no credential. |
[open_books]¶
Configures the open_books source, which finds an open-access copy of a book from its ISBN. [sources.open_books] carries its enabled, rate_per_sec and burst; this table carries where it looks. The source acts only on a work whose sole strong identifier is the ISBN (papio acquire --isbn …): a request that also has a DOI, PMID, arXiv ID or OpenAlex ID is fetched by those, and the ISBN alone is still not a fetchable identifier for any other source. A repository record is a candidate only when its own ISBN field lists the requested ISBN, it carries an open licence statement (or the repository is open access by definition), and its ORIGINAL bundle holds exactly one PDF. See ISBN-only requests.
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
dspace_servers |
string array | ["https://iris.who.int/server"] |
DSpace 7 REST roots to search, in order, at most 8 (the server's dspace.server.url; papio appends /api). Each must be an https URL with no credentials, query or fragment (plain http only for a loopback host), and none may repeat. An empty list searches no DSpace server. The default is the World Health Organization's IRIS. Listing a repository that also holds restricted items is safe: an item without an open licence statement is never a candidate. |
oapen |
boolean | false |
Also search the OAPEN Library, whose books are open access by definition. Off by default: its REST API refuses some networks (HTTP 403), and this lookup has not been verified against the live service. When on, a refusal is recorded against the source and the job carries on. |
[actions]¶
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
stale_after_seconds |
integer seconds | 604800 (7 days) |
How long an open human action may wait before listings report it stale. 0 selects the default; a negative value is rejected, and values above 31536000 seconds (365 days) are rejected. papio actions list marks stale rows (stale and age_seconds in --json), and nothing else happens: papio never cancels, expires, or sweeps a handoff on a timer. |
This is deliberately not browser.action_expiry_seconds. That one (30 minutes by
default) is a reminder cadence — how soon to nudge you again about an open
action — so reusing it as a staleness threshold would report a handoff queued
over lunch as abandoned. This one answers "has anyone given up on this?", which
is measured in days.
[drive]¶
The paced drive opens parked handoffs for you, one paper at a time, oldest
first, through the same path papio actions open uses. The extension's own
handoff settings still decide where the tab opens (the work window or
papio's tab group). The drive never accepts terms, never submits a delivery
request, and never resolves an identity review. It only puts the handoff on
screen, as an operator's open would.
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
enabled |
boolean | false |
Turns the paced drive on. Off unless set: opening human work with nobody asking is an operator's decision for one machine. |
max_opens_per_hour |
integer | 10 |
Maximum paced opens in a rolling hour. 0 selects the default. Must be in 0..60. |
job_backoff_hours |
integer hours | 6 |
How long the drive leaves a paper it opened before it may open that paper again, whatever the result was. 0 selects the default. Must be in 0..168. |
sign_in_wait_minutes |
integer minutes | 10 |
How long a paced paper's institutional sign-in may wait for its return. After that the drive settles that paper (drive.sign_in_stalled), cools its route for job_backoff_hours, and moves on. When the two most recent paced papers, on two different DOI prefixes, both stall this way, the drive pauses and sends one decision_opened notification; it resumes by itself when any sign-in returns. 0 selects the default. Must be in 0..1440. |
settle_minutes |
integer minutes | 30 |
How long one paced open counts as in flight when the browser reports nothing about it. 0 selects the default. Must be in 0..1440. |
The drive opens nothing while any of these holds, and papio drive status
names each one as a blocker:
- it is disabled, or paused by
papio drive pauseor because two paced papers in a row stalled at the sign-in; - the time is inside
notify.quiet_hours; - no browser holds the papio session;
- the hourly bound is spent;
- its previous open is still in flight;
- any browser claim or effect permit is live, including one an operator started;
- a sign-in is open and still inside
sign_in_wait_minutes; - no parked job is eligible.
Eligible jobs are those awaiting a person on one open openurl_handoff, and
those on one open manual_download that papio jobs redrive would accept; the
drive redrives such a job through the same store function first. When that
redrive sends the job back to resolving (a manual download left by an
open-access route, or a job whose institution has already reported no
entitlement), the drive opens nothing on that pass: resolving offers the
open-access handoff again when that route remains. It never opens
an openurl_available advisory, because that link opens outside papio's
window. It skips a job it opened within job_backoff_hours, and every job
behind a provider host that is refusing this browser: a host in a
browser.provider_cooldown, or a job with a Cloudflare-style challenge in the
last 10 minutes. The skip covers the papers that went to that host and every
paper under the same DOI prefix. A paced paper whose sign-in stalled cools its
own route the same way.
Every paced open records drive.paced_open on the job and, when it opens a
handoff, handoff.opened with principal pacer. Pauses and resumes are the
system events drive.paused and drive.resumed. papio drive resume always
holds: sign-in evidence recorded before it never pauses the drive again.
This section is strict-mode configuration. Deploy the binary that understands
it together with the configuration change; an older daemon rejects the whole
file when [drive] is present.
[sources.<name>]¶
[sources] is a map of resolver policies. Its keys are whitelisted
separately from papio's general strict-unknown-field rejection: the map
itself decodes freely, and validate() rejects an unrecognized key at load
with the list below. The valid names are arxiv, europepmc, unpaywall,
openalex, core, crossref_tdm, crossref_metadata, retraction_watch,
semanticscholar, openaire, and open_books. One further name, openalex_content, is
a removed key: an earlier papio release wrote it into Default() and no
adapter for it ever shipped, so a config carrying it loads normally and
drops the key silently rather than breaking on upgrade — it is not a valid
key to add yourself. For arxiv and semanticscholar, enabled governs the
acquisition resolver. Selection as a search backend is separate and lives in
[discovery]; Semantic Scholar search also uses this section's resolved
credential. For openaire, candidates come from
the OpenAIRE Graph (metadata licensed CC-BY, acknowledged here and in
candidate provenance); the keyless public limit is 60 requests/hour — the
default rate_per_sec honors it. A resolved registered-service client pair
raises the effective pacing; a short-lived personal token does not. Each named section accepts these keys:
| Key | Type | Default | Effect and constraints |
|---|---|---|---|
enabled |
boolean | source-specific; see below | Enables the resolver policy. |
credential_ref |
string | empty | OS-store or environment reference for OpenAlex, Semantic Scholar, CORE, Crossref TDM or OpenAIRE. Cannot coexist with literal credential fields. Resolved before source pacing and quota identity are chosen. |
api_key |
string | empty | Legacy credential or token; use papio config credentials migrate to move it into the shared store. CORE and Crossref TDM require a key; OpenAlex supports a smaller keyless allowance and also needs email. For openaire this holds a personal access token, which OpenAIRE expires one hour after issuing it — usable for a manual check, not for unattended operation; use client_id/client_secret instead. |
client_id |
string | empty | Legacy OpenAIRE registered-service client id. Migrate it together with client_secret into one atomic record. Unlike a personal access token these credentials do not expire, so this is the only OpenAIRE credential that survives unattended operation. Must be set together with client_secret; half a pair is ignored. |
client_secret |
string | empty | Legacy secret paired with client_id. papio exchanges the pair for a short-lived access token at OpenAIRE's AAI endpoint and re-exchanges it before expiry; the secret is never logged or echoed in an error. |
rate_per_sec |
number | source-specific; see below | Per-source request-rate budget. |
burst |
integer | source-specific; see below | Per-source burst budget. |
max_cost_usd |
number | 0 |
Monthly budget for paid sources. 0 means unmetered. |
daily_credit_fraction |
number | 0 (OpenAlex default 0.5) |
Fraction of the provider's observed daily credit limit the daemon may spend before the daily fuse parks the identity. Must be in [0,1]. OpenAlex ships 0.5 in built-in defaults; other sources default to 0 until a fuse adopts them. |
daily_credit_limit |
integer credits | 0 |
Absolute operator override and hard maximum for the daily credit fuse; the fraction may never exceed this cap. 0 means no absolute ceiling (unmetered allowance from the observed limit × fraction). The egress debit still happens when metering is active. |
sibling_title_search |
boolean | false |
OpenAlex fuzzy sibling title search — a 10-credit search= query that looks for an open-access copy of a paywalled work under a different DOI. Off by default, because each search costs 10 credits and few searches find a copy that passes validation. When off, the hop makes no request at all and reports that it made none. Set true under [sources.openalex] to turn it back on. |
allow_keep_alives |
boolean | false |
When false (default), metadata transports open one connection per request — no HTTP keep-alive reuse, the replay-hygiene posture. Set true only with evidence that a high-rate source needs reuse; it is an opt-out of the safe default and requires per-source policy justification. HTTP/1-only negotiation is not configurable. |
base_url_for_dev |
string URL | empty | Test/development endpoint override. If set, it must be a plain http URL whose host is exactly localhost, a 127.x.x.x address, or [::1] (no userinfo); do not use it for a remote production endpoint. |
| ### Built-in source defaults |
| Source name | enabled | rate_per_sec | burst |
| arxiv | true | 1 | 1 |
| europepmc | true | 2 | 2 |
| unpaywall | true | 1 | 1 |
| openalex | true | 2 | 2 |
| openaire | true | 0.016 | 1 |
| core | false | 0.4 | 1 |
| crossref_tdm | false | 1 | 1 |
| crossref_metadata | true | 1 | 1 |
| retraction_watch | true | 1 | 1 |
| semanticscholar | true | 1 | 1 |
| open_books | true | 0.5 | 1 |
openaire's rate_per_sec looks oddly precise because it is pinned to a documented
ceiling: OpenAIRE allows 60 requests per hour to unauthenticated callers, and
0.016 × 3600 = 57.6. Do not raise burst to make bursty demand go through — the
hourly budget is burst + rate_per_sec × 3600, so at this rate a burst above about 2
would breach the published ceiling.
To get more OpenAIRE throughput, authenticate: 7,200 requests per hour is 120× the keyless allowance, and it is free. OpenAIRE issues two kinds of credential and only one of them can run a daemon:
- Fill in the personal information form — the newer registration flow gates both credential types behind it.
- Go to Registered Services, click + New Service, pick the Basic security level, and create it. Copy the Client ID and Client Secret it shows you.
- Save them as one
openaire_clientrecord withpapio config credentials set sources.openaire --key-stdin, using the JSON shape in Integration credentials.
papio exchanges that pair for an access token as needed and refreshes it before it
expires. The alternative credential — a personal access
token — can be stored as an openaire_token record, but OpenAIRE expires it one hour after issuing it, so it is only
useful for a manual check. papio doctor warns when api_key is carrying one.
Note that OpenAIRE reports x-ratelimit-limit: 7199 in its responses even to
unauthenticated requests, so that header is not a safe basis for choosing
rate_per_sec. Use the documented ceiling for the tier you are actually in.
Resolving a complete client credential pair also raises rate_per_sec to 1.9 and burst
to 5 (6,845 requests/hour against the documented 7,200), because a credential that
authenticated but changed nothing observable would be two knobs for one intent. An
explicit rate_per_sec or burst in your config always wins. A personal access
token in api_key does not raise pacing: it authenticates for an hour, and pacing
to the authenticated ceiling on a credential that can vanish mid-hour would leave
papio running at 120× an unauthenticated allowance.
Watch configuration¶
There is no [watch] section or watch-specific key in papio's TOML config.
Watch query, year filters, OA filter, collection, cadence, and per-run cap are
stored with each watch created by papio watch add or the corresponding MCP
tool. Use papio watch list to inspect them and papio watch remove <id> to
remove one.
Validation and file permissions¶
papio validates configuration when loading and saving it. It writes the config
file with mode 0600 and its config directory with mode 0700; doctor reports
a configuration permission failure when group or other read bits are present.
Use papio doctor rather than weakening these permissions to diagnose a setup
problem.