GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — pyproc/skills/control-pyproc/references/python-sdk.md at main · eddmpython/pyproc

Expand file tree

/

python-sdk.md

Copy path
More file actions
More file actions

Latest commit

 

History

History
History
457 lines (367 loc) · 22.1 KB
/

python-sdk.md

Copy path

File metadata and controls

457 lines (367 loc) · 22.1 KB
Raw
Copy raw file
Download raw file
Outline
Edit and raw actions

python-sdk

Contents

  • Python SDK
  • Install
  • Persistent Python and checkpoint recovery
  • Execution Memory
  • Rehearse-Commit transactions
  • Transactional AppSpace
  • ReplayGraph Worlds
  • Browser automation and screenshots
  • PyProc Eyes from Python
  • Proof-Carrying Motor from Python
  • Cancellation and errors
  • Returning to a running host
  • Distribution verification
  • Repository experience verification

Python SDK

The official pyproc-control Python package starts and controls pyproc without JavaScript application code. The SDK has no runtime dependency and supports Python 3.10 or newer.

Every release builds three Python distributions from the same commit:

  • A platform wheel for Windows x64 (win_amd64) or Linux x64 (manylinux_2_28_x86_64, glibc 2.28 or newer) carries the host itself: the npm pyproc package of the same version and a checksum-pinned official Node.js runtime. It needs no Node, npm, or PATH setup, and it installs the pyproc-control and pyproc-mcp commands.
  • The pure wheel (py3-none-any) and the source distribution carry only the protocol client, typed values, cancellation, and attachment verification. They start the pyproc-control command that the npm pyproc package puts on PATH, or an explicit command.

Install

python -m pip install \
  "https://github.com/eddmpython/pyproc/releases/download/v0.0.46/pyproc_control-0.0.46-py3-none-win_amd64.whl"

On Linux x64 install pyproc_control-0.0.46-py3-none-manylinux_2_28_x86_64.whl from the same release. The platform wheel is the whole product: PyProcClient.start uses its own host even when another pyproc-control is on PATH, because that host is the exact release this client was built with. The same commit always yields the same wheel bytes, the release's python-distributions-manifest.json lists each SHA-256, and every asset has a GitHub build provenance attestation (gh attestation verify <file> --repo eddmpython/pyproc).

On other platforms, install the pure wheel and the npm package at the same exact version:

npm install --save-exact [email protected]
python -m pip install \
  "https://github.com/eddmpython/pyproc/releases/download/v0.0.46/pyproc_control-0.0.46-py3-none-any.whl"

PyPI is not an installation source yet. Use the exact-version asset URL because floating release URLs are outside the reproducible installation contract.

Create the version 1 manifest with Machine Entrance. A Python-only recipe expands to "browser": { "enabled": false }. Run the complete doctor before use:

pyproc-mcp init --recipe pythonOnly
pyproc-control doctor --config ./.pyproc/manifest.json

With the pure wheel, run the same commands through npx.

from pyprocControl import PyProcClient

report = PyProcClient.doctor(".pyproc/manifest.json")
print(report["next"]["firstResult"])

doctor() returns the complete effect-free report, including a structured next.firstResult that maps the same machine.run input to shell, JavaScript, Python, and MCP. It returns representable blocking reports with ok == False, so a caller can inspect blocking before deciding whether to start. check() remains the lighter startup-configuration compatibility check.

Without a platform wheel, the npm bin directory must be on PATH. An embedded application can always pass an explicit command list such as command=[nodePath, controlScriptPath].

Persistent Python and checkpoint recovery

from pyprocControl import PyProcClient

with PyProcClient.start(".pyproc/manifest.json") as client:
    client.runPython("prepared = [10, 20, 30]")
    checkpoint = client.saveCheckpoint()

    try:
        client.runPython("prepared.append(999)\nraise ValueError('failed attempt')")
    except Exception:
        client.restoreCheckpoint(checkpoint.output["index"])

    result = client.runPython("prepared")
    print(result.output["value"])

ControlResult contains terminal="completed", output, outcome, and an ordered tuple of verified attachments. ControlError.terminal is rejected, partial, outcomeUnknown, or cancelled while preserving the protocol code, outcome, and retryability. Machine state persists for the life of the client. reset() restores the prepared boot checkpoint.

Execution Memory

Add --execution-memory-root /absolute/private/pyproc-memory to pyproc-mcp init. The Python client then uses the same immutable revisions as JavaScript and MCP:

project = {
    "workspaceId": "workspace:forecast",
    "commit": "exact-commit",
    "treeSha256": "sha256:...",
    "diffSha256": "sha256:...",
    "untracked": False,
}
created = client.createExecutionSession("session:forecast", project)
opened = client.openExecutionSession("session:forecast")
assert created.output["contentSha256"] == opened.output["contentSha256"]

checkpointExecutionSession, completeExecutionSession, listExecutionSessions, inspectExecutionSession, exportExecutionHandoff, and importExecutionHandoff map directly to the Control operations. The registry persists state, not the client object. See Execution Memory.

Rehearse-Commit transactions

An effect-enabled profile exposes the same transaction lifecycle as JavaScript and MCP:

prepared = client.prepareEffectTransaction(intentInput)
rehearsed = client.rehearseEffectTransaction(
    intentInput["transactionId"],
    prepared.output["transaction"]["contentSha256"],
    {"mode": "computed", "code": "6 * 7", "expectedValue": "42"},
)
approved = client.approveEffectTransaction(
    intentInput["transactionId"], rehearsed.output["contentSha256"], signedGrant)
terminal = client.commitEffectTransaction(
    intentInput["transactionId"], approved.output["contentSha256"])
sealed = client.sealEffectTransaction(
    intentInput["transactionId"], terminal.output["contentSha256"], evidencePackDir)

inspectEffectTransaction and listEffectTransactions are read-only lifecycle queries. The Python package does not mint approval authority; pass a canonical grant produced by the separately trusted issuer. Recovery from a durable sending state never repeats the provider call. See Rehearse-Commit.

Transactional AppSpace

An AppSpace-enabled FrameSpace profile exposes attachApp, checkpointApp, branchApp, restoreApp, adoptApp, inspectApp, listAppPairs, stageAppEffect, and finalizeAppEffect. These methods send the same canonical app.* operations as JavaScript and MCP. The installed AppSpace gate reopens the durable list through all three clients and requires the same active pair digest.

The Python client does not implement an independent app state machine. Live pair restore still requires the originating control process and its in-process Machine checkpoints. See Transactional AppSpace.

ReplayGraph Worlds

A ReplayGraph-enabled profile exposes importReplayGraphRecording, createReplayGraphAppWorld, captureReplayGraphAppBranch, openReplayWorld, inspectReplayWorld, listReplayWorldEdges, traverseReplayWorld, checkpointReplayWorld, restoreReplayWorld, evaluateReplayWorld, inspectReplayWorldCoverage, and listReplayGraphs. These methods send the same canonical world.* operations as JavaScript and MCP.

The Python client owns no independent graph state. Durable revisions live under the shared Execution Memory root; open cursor references and checkpoints remain in the originating Control process. The installed product gate imports and reopens the same root through Python, JavaScript, and MCP without calling a live provider. See ReplayGraph Worlds.

Browser automation and screenshots

Browser operations appear only when the manifest grants automation authority. Risk is fixed by the product catalog and must be acknowledged on each effect:

from pyprocControl import ActionConvergenceReceipt

with PyProcClient.start(".pyproc/manifest.json") as client:
    opened = client.openTarget(
        "https://example.test",
        expectedRisk="externalEffect",
        waitUntil="load",
    )
    attached = client.attachSession(opened.output["targetRef"])
    captured = client.act(attached.output, [
        {"kind": "screenshot", "format": "png", "expectedRisk": "read"}
    ])

    png = captured.attachments[0]
    assert png.mimeType == "image/png"
    with open("capture.png", "wb") as file:
        file.write(png.bytes)

    client.detachSession(attached.output)
    client.closeTarget(opened.output["targetRef"])

The client does not expose attachment bytes until chunk order, byte length, MIME type, and SHA-256 match the terminal descriptor. The JSON output retains the opaque artifact reference for bounded reads and deletion.

Large legacy semantic inventories use the same bounded continuation contract:

pages = []
observed = client.observe(attached.output, {
    "expectedRisk": "read", "mode": "all", "maxNodes": 500})
while True:
    page = observed.output.get("result", observed.output)
    pages.append(page)
    if page["inventory"]["complete"]:
        break
    observed = client.observe(attached.output, {
        "expectedRisk": "read", "continuationRef": page["continuationRef"]})

nodes = [node for page in pages for node in page["nodes"]]
assert len(nodes) == pages[-1]["inventory"]["total"]

Pass no first-page options with continuationRef. Every page must retain one receiptSha256, and the final canonical sorted-key JSON digest must equal nodesSha256. A document replacement raises ControlError with code AUTOMATION_OBSERVATION_CONTINUATION_STALE; do not mark the collected prefix complete.

PyProc Eyes from Python

client.perception(sessionRef) binds provider-neutral APX graph and situation representations to one attached session. It uses the same automation.observe and automation.act operations as MCP and native JavaScript clients, so Python does not gain a second automation meaning or a raw CDP entrance.

with PyProcClient.start(".pyproc/manifest.json") as client:
    opened = client.openTarget(
        "https://example.test",
        expectedRisk="externalEffect",
        waitUntil="load",
    )
    attached = client.attachSession(opened.output["targetRef"])
    eyes = client.perception(attached.output)

    situation = eyes.situate({"requirements": [{
        "requirementRef": "requirement:save",
        "select": {"role": "button", "name": "Save", "actionable": True},
        "need": ["fact", "affordance"],
        "cardinality": "one",
    }]})
    save = situation.requirement("requirement:save").oneAffordance("click")
    result = eyes.actAffordance(
        save,
        intent="Save the document",
        verify={"entityAppeared": {"role": "status", "nameContains": "Saved"}},
    )
    assert result.output["actions"][0]["result"]["evidence"]["verification"]["state"] == "confirmed"
    receipt = ActionConvergenceReceipt.fromMapping(result.output["actions"][0]["convergence"])
    assert receipt.maxAttempts == 2 and receipt.effectRetries == 0

SituationResult exposes immutable requirements, facts, affordances, and unknowns. Only an authorized SituationAffordance can be passed to actAffordance; page-reported affordances remain content. The older PerceptionEntity exposes entityRef, locatorRef, kind, role, name, and actionable properties. query(...).one() checks APX query.matched and rejects zero or multiple matches even when the byte budget returns only one candidate. whatChanged(observationRef) requests a delta, while explainActionability(entityRef) narrows the graph to semantic, geometry, and interaction facts. Observation identity never grants action authority, and locator capability remains short-lived.

Each requirement exposes candidateEvidence from the pre-projection candidate universe. Complete inventories carry an exact count and digest; incomplete inventories carry only a lower bound and authorize no effect. Verified action output includes send-boundary timing, independent input-release evidence, and observationCoverage for focused entities and monotonic event windows. An incomplete window never proves that an event was absent.

Use ActionConvergenceReceipt.fromMapping(...) for a successful action terminal or ControlError.details["convergence"]. Native CDP uses output["actions"][0]; FrameSpace uses output["results"][0]. The receipt fixes two candidate attempts, one reobservation, zero effect retries, and a 30000 ms first-effect deadline while reporting the actual effect count and timing.

Native CDP supports verified pixel-on-demand attachments. FrameSpace supports APX through temporal level L3 but rejects visual inference. See the APX 1.0 product contract for the graph and evidence contract.

Proof-Carrying Motor from Python

openMotorTask() binds target, session, Situation, artifact, and cleanup ownership to one task, with the same meaning as the JavaScript task, and shares the same durable Motor records and receipt digests as JavaScript and MCP:

from pyprocControl import (ActuationIntent, MotorAuthority, MotorPolicy, MotorTarget, actuationDigest)

with client.openMotorTask(url="https://app.example/work", waitUntil="load") as task:
    observed = task.situate({"requirements": [{
        "requirementRef": "requirement:save",
        "select": {"role": "button", "name": "Save", "actionable": True},
        "need": ["fact", "affordance"], "cardinality": "one",
    }]})
    if not task.diagnoseAmbiguity(observed, "requirement:save").canExecute:
        raise RuntimeError("caller refinement is required")
    situation = observed.situation
    affordance = next(entry for entry in situation["affordances"]
                      if entry["kind"] == "authorized" and entry["action"] == "click")
    motor = task.execute(observed, "requirement:save", ActuationIntent(
        intent="activate",
        target=MotorTarget(spaceRef=spaceRef, entityRef=situation["requirements"][0]["entityRefs"][0],
                           worldRef=situation["worldRef"], surfaceEpoch=f"document:{situation['documentEpoch']}"),
        desired={"activated": True},
        authority=MotorAuthority(actionCapabilityRef=affordance["capabilityRef"]),
        policy=MotorPolicy(allowedActuatorKinds=("browserInput",)),
        expectedTransition={"entityAppeared": {"role": "status", "name": "saved"}},
    ))
    receipt = dict(motor.output["receipt"])
    assert actuationDigest({k: v for k, v in receipt.items() if k != "receiptSha256"}) == receipt["receiptSha256"]

The task executes only a Situation it observed whose requirement settled on one complete target; otherwise execute() raises TypeError, and diagnoseAmbiguity() names the caller-owned predicates that would refine it. Leaving the with block (or close()) detaches the session, deletes the artifacts the task did not retain, and closes a target it opened; it returns a MotorTaskCleanup and never retries an effect. A closed task raises RuntimeError("Motor task session is closed"). executeMotor() still takes an intent mapping directly.

ActuationIntent carries the exact Situation world, entity, surface epoch, action capability, desired final state, expected transition, and actuator allowlist. canonicalActuationJson() and actuationDigest() produce the Control host's canonical JSON and digest byte for byte, so a receipt can be checked locally. Python does not receive raw coordinates or provider handles. Windows physical input additionally requires acquireMotorControl() and a one-shot control lease. Use revokeMotorControl() before execution when the surrounding product cancels the task. See the Motor guide.

Cancellation and errors

requestAsync returns a ControlRequest:

from pyprocControl import ControlError

request = client.requestAsync("machine.run", {
    "code": "import time\ntime.sleep(30)"
})
request.cancel("deadline reached")

try:
    request.result()
except ControlError as error:
    print(error.code, error.outcome, error.retryable)

Every synchronous timeout= is an effect-safe request deadline. When the local wait expires, the client sends one protocol cancel and waits for the canonical terminal. A queued request ends as notSent; a delivered effect normally ends as non-retryable outcomeUnknown. If the server does not settle after cancellation, the client closes the connection and raises non-retryable CONTROL_TIMEOUT with outcomeUnknown.

If the transport ends while a request is pending, or a request frame write cannot prove full delivery, the SDK raises non-retryable CONTROL_CONNECTION_LOST with outcomeUnknown. A request rejected before writing because the connection is already known to be unavailable is notSent. Raw pipe and EOF errors do not escape through the request surface.

Cancellation before delivery is notSent. After delivery, the product returns outcomeUnknown unless the provider proves a narrower boundary. Never retry an applied or outcomeUnknown effect automatically. Python restore does not undo browser, storage, network, download, popup, or other external effects.

ControlError exposes stable code, outcome, retryable, details, and fatal fields. Protocol framing or digest violations fail the whole client and reject every outstanding request.

Returning to a running host

A host ends with the process that started it, and its browser with it. Start it with reconnectFile when the browser sessions must outlive that process:

client = PyProcClient.start(".pyproc/manifest.json", reconnectFile="state/reconnect.json", reconnectSeconds=120)
opened = client.openTarget("https://example.test", expectedRisk="externalEffect", waitUntil="load")
sessionRef = client.attachSession(opened.output["targetRef"]).output
# Store opened.output["targetRef"] and sessionRef where the next process can read them.

If this process dies while a session is attached, the host keeps the browser and waits reconnectSeconds (60 by default, at most 1800) for a client. Another process returns to it and uses the stored references:

client = PyProcClient.reconnect("state/reconnect.json")
client.observe(sessionRef, {"expectedRisk": "read"})

The waiting host does not listen. reconnect listens on a loopback port, announces it beside the file, and the host connects to it within startupTimeout seconds, so both processes need to read and write the directory of the file. The Control Protocol reference has the exchange.

reconnect raises FileNotFoundError or ConnectionError when no host waits there or none comes in time, and ValueError when the file is not an endpoint; open a new host in that case. The returned client has no process; hostPid is the process ID of the host and connected turns false once the connection is closed or lost. Requests that were in flight when the first client was lost ended with it, so treat an effect it had sent as outcomeUnknown.

close() ends the host at once only when no session is attached, so detach the sessions first. With a session still attached, the client that started the host stops it after the close timeout, and a host left by a client that had returned to it waits until its time runs out. The file and the announcement beside it hold secret tokens: keep them in a directory only the owning user can read and write, and never log them. The wire contract is in Control Protocol.

Distribution verification

npm run test:python-sdk builds both the wheel and source distribution with pinned build tools, installs each into a separate clean virtual environment, and runs these installed-package checks:

  • the platform wheel for the running host, installed alone into a virtual environment whose PATH has no Node, starts its bundled host through pyproc-control and pyproc-mcp init, runs Python, and opens and closes a Machine session and a browser-only session;

  • strict codec positive and negative fixtures;

  • PATH-based product preflight and handshake;

  • persistent Python and checkpoint restore;

  • post-send cancellation and request ID single-use;

  • permission denial before browser effect;

  • APX query through both Native CDP and FrameSpace;

  • real browser open, attach, PNG capture, SHA-256 verification, artifact deletion, and detach.

  • provider-neutral inspectSpace().output["resources"] convergence after target close.

  • real Machine image capture, immutable Execution Memory publication, reopen, and list through the installed Python wheel.

  • signed exact-intent approval, one live HTTP effect, no resend, and verified EffectReceipt sealing through the installed Python wheel.

Chrome on Ubuntu and Edge on Windows run the same gate in CI. npm run package:python -- --tree <commit> --out <empty-directory> builds the release set, and the python-distribution-reproducibility workflow builds it on Ubuntu and Windows and requires byte-identical platform wheels.

Repository experience verification

The Python facade uses the same three Control operations as JavaScript and MCP:

repository = {
    "commit": "exact-commit-id",
    "treeSha256": "sha256:...",
    "diffSha256": "sha256:...",
    "untracked": False,
}

audited = client.auditExperience(
    "qa/eyes",
    repositoryRoot=".",
    outputDir=".pyproc/evidence/current",
    environmentId="desktop",
    repository=repository,
    motorJourneys=[{
        "receiptSha256": receipt_sha256,
        "scenarioId": "save-document",
        "checkpointId": "post-save",
    }],
)
client.replayEvidencePack(".pyproc/evidence/current")
client.verifyExperience(".pyproc/evidence/reference", ".pyproc/evidence/current")

All filesystem inputs are resolved to absolute paths by the client except outputDir, which deliberately remains relative to repositoryRoot. auditExperience requires browser authority. Verify and replay are effect-free and remain available in browser-disabled profiles. See experience verification for the repository contract and Evidence Pack format.

You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.