p8
The processes that sit in front of the database. ../../INSTALL.md covers the
database itself; nothing here is required to use it.
pip install p8 # the worker -- asyncpg, httpx, typer
pip install 'p8[content]' # + Content Server (boto3, fastapi)
pip install 'p8[agent]' # + Agent Runtime (pydantic-ai, mcp)
pip install 'p8[all]'
p8 worker --queue http # claim and execute tasks
p8 content serve # uploads, scraping, http_call execution
p8 agent serve # agents, streaming, delegation
Layout
| Module | Extra | What it is |
|---|---|---|
p8.core |
— | connecting as the caller, configuration, credential resolution |
p8.worker |
— | the step loop and the @handler registry |
p8.content |
content |
the Content Server |
p8.agentic |
agent |
the Agent Runtime |
One distribution rather than three. Three packages means a version matrix
(p8content 0.3 requiring p8core >=0.2,<0.3) resolved forever, for services
that release together and are written by the same people. Splitting later is
mechanical; merging two that have drifted is not.
The base install stays small on purpose. The common case is someone writing
their own worker, and they should not pull boto3 and pydantic-ai to do it. The
CLI imports each subpackage lazily, so p8 worker runs without either
installed and p8 content serve fails with the extra to install rather than an
ImportError.
p8.core — the one that matters
It decides whose RLS applies, and it exists because that logic was previously written three times on two different database drivers.
from p8.core import as_caller
async with as_caller(claims) as conn: # claims = the VERIFIED JWT payload
rows = await conn.fetch("select * from content.resources")
Every service connects as a low-privilege role and sets the caller's claims per transaction, exactly as PostgREST does. A service that queried as itself would bypass every policy in the collection — not by exploiting anything, just by never presenting an identity for the policies to filter on.
Transaction-local (set_config(..., true)) is not a detail: an unregistered
GUC left at session scope survives into the next transaction on a pooled
connection, so the following request would inherit the previous caller's
identity.
as_service() exists for work with genuinely no user behind it — a scheduled
poll, a reconciliation sweep. Deliberately a separate function rather than
as_caller(None), so "this query has no user" is something someone wrote down.
Writing your own worker
Most steps need no worker from you:
| kind | who runs it |
|---|---|
sql / p8ql |
nobody — executes inside Postgres |
http_call |
the built-in handler |
timer / signal / decision / sub_workflow |
the engine |
work |
you |
When you do need one, it is this loop with a handler registered — not a different program:
from p8.worker import handler, run
@handler("transcode")
async def transcode(spec, ctx):
return {"duration": await ffmpeg(ctx["run_input"]["file_key"])}
run(queue="media")
ctx comes from workflow.get_task_context(): run_input, the accumulated
context (so a later step reads an earlier step's output), task_input,
step_key, and trace_id/span_id.
The worker holds no table grants. Every interaction is a SECURITY DEFINER
function call — claim_task, get_task_context, complete_task, fail_task
— which is why "bring your own worker" is safe to offer: a compromised worker
can claim and complete tasks, and nothing else.
Raise TerminalError for what will not get better. A bad argument, a
missing credential, a 404. Anything else is retried with backoff. The worker is
the only thing that knows what a failure means, so it decides and the engine
honours the verdict.
Configuration
Environment only. Credentials by reference, never by value:
credential_ref: "LLM_API_KEY" on a task names a variable this process
resolves, so workflow.tasks stays inspectable and replayable.
| Used by | |
|---|---|
P8_DSN |
all |
P8_JWT_SECRET |
services verifying bearer tokens (same secret PostgREST uses) |
P8_QUEUE, P8_WORKER_ID, P8_POLL_SECONDS |
worker |
P8_S3_ENDPOINT, P8_S3_KEY, P8_S3_SECRET, P8_BUCKET |
content |
Deployment
One image, three entrypoints — they share p8.core, so three images would be
three builds of the same base and three tags to keep in step.
content: { image: p8/runtime:0.1, command: ["p8","content","serve"] }
agent: { image: p8/runtime:0.1, command: ["p8","agent","serve"] }
worker: { image: p8/runtime:0.1, command: ["p8","worker","--queue","http"] }
See ../../PACKAGING.md for the reasoning and the
remaining work.
Status
p8.core,p8.worker,p8.content— built, and exercised against a live PG19 instance with MinIO.p8.agentic— moved fromexperiments/agentic-runtimeunchanged (it uses relative imports throughout, so the move needed no edits). Its own tests have not been re-run under the new namespace.- No Dockerfile yet. No service-surface entries in
../../surface.sql, which bymeta/skills/spec-driven-development§7 should exist before the endpoints they describe.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file percolate_core-0.1.0.tar.gz.
File metadata
- Download URL: percolate_core-0.1.0.tar.gz
- Upload date:
- Size: 82.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b858cea39ed39fec87b2bf55d1223378da2cab045e2cfc82bca69f5aa5222e4
|
|
| MD5 |
7b5e1f3e55bf9121415ca18635b817ae
|
|
| BLAKE2b-256 |
32d033dfc2840d9ae651cb1bd3e9509e403b1e48ded64636f614163f0aaf90b1
|
File details
Details for the file percolate_core-0.1.0-py3-none-any.whl.
File metadata
- Download URL: percolate_core-0.1.0-py3-none-any.whl
- Upload date:
- Size: 110.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e6cc7a9a16b05886b125d7d1b41ba179333f385c36f8a37db54818322e0e87a
|
|
| MD5 |
f444962d085a73c3e50514ddfe3fa2a1
|
|
| BLAKE2b-256 |
2eaf60f7a37005083bd34c0ab1915694464491f35885dca38fe1b5503da26465
|