@zieglar/pi-chat-exchange 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/bin/mcp-stdio.js +196 -0
- package/dist/control/debug-log.js +67 -0
- package/dist/control/mcp/debug-transport.js +48 -0
- package/dist/control/mcp/messaging-tools.js +278 -0
- package/dist/control/mcp/ping.js +43 -0
- package/dist/control/mcp/server.js +153 -0
- package/dist/core/authorization.js +32 -0
- package/dist/core/reference.js +32 -0
- package/dist/core/rules.js +468 -0
- package/dist/endpoints/pi/contract.js +2 -0
- package/dist/endpoints/pi/session-host.js +39 -0
- package/dist/host/bound-endpoint.js +176 -0
- package/dist/host/endpoint-host.js +839 -0
- package/dist/host/mcp-binding-config.js +209 -0
- package/dist/host/ownership-recovery.js +257 -0
- package/dist/index.js +11 -0
- package/dist/protocol/capabilities.js +46 -0
- package/dist/protocol/endpoint-capabilities.js +15 -0
- package/dist/protocol/entities.js +185 -0
- package/dist/protocol/formats.js +13 -0
- package/dist/protocol/idempotency.js +93 -0
- package/dist/protocol/identity.js +130 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/lifecycle.js +125 -0
- package/dist/protocol/outcomes.js +55 -0
- package/dist/runtime/desktop-manifest.js +65 -0
- package/dist/runtime/diagnostics.js +34 -0
- package/dist/runtime/executables.js +222 -0
- package/dist/storage/capability.js +221 -0
- package/dist/storage/connection.js +94 -0
- package/dist/storage/creation-request.js +176 -0
- package/dist/storage/creation.js +553 -0
- package/dist/storage/current-schema.js +397 -0
- package/dist/storage/delivery-ack.js +177 -0
- package/dist/storage/errors.js +9 -0
- package/dist/storage/filesystem.js +116 -0
- package/dist/storage/inbox-discovery.js +135 -0
- package/dist/storage/messaging.js +156 -0
- package/dist/storage/migration.js +258 -0
- package/dist/storage/ownership.js +789 -0
- package/dist/storage/participant-identity.js +127 -0
- package/dist/storage/participant.js +700 -0
- package/dist/storage/recovery.js +99 -0
- package/dist/storage/runtime.js +113 -0
- package/dist/storage/schema.js +556 -0
- package/dist/storage/session-state.js +75 -0
- package/dist/storage/store.js +450 -0
- package/dist/storage/transaction.js +199 -0
- package/dist/storage/trust-root.js +164 -0
- package/dist/tunnel/background-manager.js +1727 -0
- package/dist/tunnel/codes.js +79 -0
- package/dist/tunnel/health.js +75 -0
- package/dist/tunnel/keychain.js +526 -0
- package/dist/tunnel/managed-binding.js +454 -0
- package/dist/tunnel/manager-panel.js +249 -0
- package/dist/tunnel/manager-ui.js +341 -0
- package/dist/tunnel/manager.js +372 -0
- package/dist/tunnel/messaging-activation-ui.js +85 -0
- package/dist/tunnel/messaging-config.js +246 -0
- package/dist/tunnel/multi-session-admission.js +31 -0
- package/dist/tunnel/multi-session-policy.js +314 -0
- package/dist/tunnel/offline-client.js +104 -0
- package/dist/tunnel/profile.js +319 -0
- package/dist/tunnel/redact.js +9 -0
- package/dist/tunnel/secure-input.js +318 -0
- package/dist/tunnel/session-process-mutex.js +338 -0
- package/dist/tunnel/setup-ui.js +443 -0
- package/dist/tunnel/setup.js +734 -0
- package/dist/tunnel/state.js +127 -0
- package/docs/architecture.md +170 -0
- package/docs/chatgpt-installation.md +149 -0
- package/docs/delivery-ack-contract.md +86 -0
- package/docs/independent-install.md +156 -0
- package/docs/multi-session-usage.md +77 -0
- package/docs/ownership-contract.md +429 -0
- package/docs/protocol-v2.md +269 -0
- package/docs/releases/0.2.0.md +59 -0
- package/docs/storage-contract.md +1241 -0
- package/docs/storage-security.md +138 -0
- package/docs/tunnel-manager-background.md +169 -0
- package/docs/tunnel-setup.md +214 -0
- package/native/tunnel-keychain.swift +432 -0
- package/package.json +77 -4
- package/pi-extension/README.md +125 -0
- package/pi-extension/allowlist.example.json +11 -0
- package/pi-extension/index.ts +2312 -0
- package/pi-extension/multi-session-commands.ts +93 -0
- package/pi-extension/multi-session-runtime.ts +75 -0
- package/pi-extension/tui-keys.ts +40 -0
- package/scripts/reconcile-session-process-mutex.mjs +262 -0
- package/scripts/recover-stale-ownership.mjs +91 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# B3-P2 Ownership Contract
|
|
2
|
+
|
|
3
|
+
Status: **clarified by CE-0002-B3-P2A-R1** (this document is the versioned
|
|
4
|
+
record of the P2 ownership design decisions). Scope: the private durable
|
|
5
|
+
storage plane only. No delivery, adapter, or Pi runtime surface is defined
|
|
6
|
+
here.
|
|
7
|
+
|
|
8
|
+
## Provenance
|
|
9
|
+
|
|
10
|
+
P2A shipped without an accepted B3-D0 design record in the repository; the
|
|
11
|
+
independent review (CE-0002-B3-P2A-INDEPENDENT-REVIEW-1) found the lease time
|
|
12
|
+
model unsafe (MAJOR-1) and five undecided design questions (Q1–Q5). R1 records
|
|
13
|
+
the decisions below. Each entry distinguishes **accepted existing behavior**
|
|
14
|
+
(from B2/P1A/P1B/P2A as implemented and reviewed) from the **newly clarified
|
|
15
|
+
decision** that governs from R1 onward.
|
|
16
|
+
|
|
17
|
+
## Q1 — Owner replacement is takeover
|
|
18
|
+
|
|
19
|
+
**Decision (new):** Replacing *any* existing Owner — including one whose lease
|
|
20
|
+
has expired — belongs to the P2B `takeover` scope. `attach` may acquire
|
|
21
|
+
ownership only when **no ownership row exists** for the Participant.
|
|
22
|
+
|
|
23
|
+
- An existing live row refuses acquisition with `OWNERSHIP_HELD` (accepted P2A
|
|
24
|
+
behavior, unchanged).
|
|
25
|
+
- An existing expired row — including a legacy v2 row, which is expired at
|
|
26
|
+
every instant — refuses acquisition with `OWNERSHIP_TAKEOVER_REQUIRED`
|
|
27
|
+
(new). No fencing token is allocated by a refused acquisition.
|
|
28
|
+
- `takeover` remains `CAPABILITY_SCOPE_UNSUPPORTED`; no takeover implementation
|
|
29
|
+
exists in R1.
|
|
30
|
+
- Consequence: after a lease expires, the Participant has a durable,
|
|
31
|
+
fenced, non-authorizing ownership record until P2B takeover exists. This is
|
|
32
|
+
deliberate fail-closed behavior, not an oversight.
|
|
33
|
+
|
|
34
|
+
## Q2 — Resume authority is Session-bound
|
|
35
|
+
|
|
36
|
+
**Decision (new):** `resume` authority must ultimately be Session-bound. R1
|
|
37
|
+
implements the minimum safe foundation:
|
|
38
|
+
|
|
39
|
+
- Schema v3 adds `participant_capability.session_id` (nullable; NULL for all
|
|
40
|
+
non-resume scopes and for legacy v2 rows).
|
|
41
|
+
- Issuing a `resume` capability requires `sessionId`, and the Session must be
|
|
42
|
+
currently bound to the Participant at issuance (checked in the same
|
|
43
|
+
transaction). `sessionId` is refused for every other scope.
|
|
44
|
+
- Every `resume` authorization compares the durable binding with the presented
|
|
45
|
+
Session inside the mutation's transaction; a legacy unbound row, a missing
|
|
46
|
+
presentation, or a mismatch classifies `SESSION_MISMATCH` and refuses.
|
|
47
|
+
- Revoking a `resume` capability requires presenting its bound Session.
|
|
48
|
+
- **Non-goal:** the Session ID is not a credential. No operation is authorized
|
|
49
|
+
by a Session ID alone; the bearer token, Participant binding, scope, trust
|
|
50
|
+
generation, revocation, and expiry checks are all unchanged.
|
|
51
|
+
|
|
52
|
+
P2A's Participant-wide resume tokens do not survive: on a migrated store, a
|
|
53
|
+
legacy (NULL) resume capability can never authorize. The host issues new
|
|
54
|
+
Session-bound resume capabilities. Fail closed, no silent widening.
|
|
55
|
+
|
|
56
|
+
## Q3 — Lease term boundaries are durably persisted
|
|
57
|
+
|
|
58
|
+
**Decision (new):** the lease term start is persisted
|
|
59
|
+
(`participant_ownership.lease_started_at`), written at acquisition and updated
|
|
60
|
+
at every renewal. Default TTL 120 s; allowed TTL 30–3600 s (unchanged from
|
|
61
|
+
P2A).
|
|
62
|
+
|
|
63
|
+
- Lease evaluation compares the operation clock against the persisted
|
|
64
|
+
boundaries `[leaseStartedAt, expiresAt)` only. A request TTL is used solely
|
|
65
|
+
to compute a *new* expiry at acquisition or renewal; it never reinterprets a
|
|
66
|
+
stored term. (This remediates R1-finding MAJOR-1: the P2A model derived the
|
|
67
|
+
term start as `expiresAt − requestTTL`, which falsely refused valid Owners
|
|
68
|
+
when TTLs differed between calls and allowed a rolled-back clock to extend a
|
|
69
|
+
lease by widening the presented TTL.)
|
|
70
|
+
- Renewal never shortens the term: a new expiry earlier than the current one
|
|
71
|
+
is refused (`LEASE_TERM_SHORTENED`).
|
|
72
|
+
- Exact-expiry refusal is preserved: `now >= expiresAt` is `LEASE_EXPIRED`.
|
|
73
|
+
- **Known limit (unchanged, documented):** a wall-clock rollback that lands
|
|
74
|
+
*inside* the current term cannot be distinguished from a valid instant
|
|
75
|
+
without a persisted monotonic observation. R1 does not claim detection of
|
|
76
|
+
every rollback. Refusals are zero-write.
|
|
77
|
+
- Legacy v2 ownership rows (NULL `lease_started_at`) are treated as expired at
|
|
78
|
+
every instant: no lease start is fabricated from an untrusted TTL, and no v3
|
|
79
|
+
operation authorizes against them.
|
|
80
|
+
|
|
81
|
+
## Q4 — Expired Owners cannot release
|
|
82
|
+
|
|
83
|
+
**Decision (confirmed existing P2A behavior, now contractual):** release
|
|
84
|
+
requires a live lease. An expired Owner's release refuses `LEASE_EXPIRED`; the
|
|
85
|
+
row persists (fenced, non-authorizing) until takeover.
|
|
86
|
+
|
|
87
|
+
## Q5 — A current Owner's Session binding cannot be deleted directly
|
|
88
|
+
|
|
89
|
+
**Decision (confirmed existing P2A behavior, now contractual):** P2A/P2A-R1
|
|
90
|
+
has no detach operation. A detached Session that is the current Owner is
|
|
91
|
+
inconsistent durable state (`STORAGE_OWNERSHIP_CORRUPT`, refused, never
|
|
92
|
+
repaired). Detach semantics (including the detach-versus-ownership ordering
|
|
93
|
+
rule) are deferred to P2B.
|
|
94
|
+
|
|
95
|
+
## Accepted existing behavior carried into R1 unchanged
|
|
96
|
+
|
|
97
|
+
- Session identity: opaque, non-secret `ses_<ULID>`; never a Participant
|
|
98
|
+
identity, never a credential. A Session belongs to exactly one Participant
|
|
99
|
+
(now also enforced by the durable `participant_session_unique_session_id`
|
|
100
|
+
index); a Participant may hold many historical Sessions. Binding never
|
|
101
|
+
confers ownership; Participant creation never creates Sessions or ownership.
|
|
102
|
+
- Fencing: first token 1; strictly monotonic high-water per Participant;
|
|
103
|
+
survives restart, expiry, and release; never reused; allocation past
|
|
104
|
+
`Number.MAX_SAFE_INTEGER` refuses (`FENCING_EXHAUSTED`); fencing and the
|
|
105
|
+
ownership write are one atomic unit; an ownership row must carry exactly the
|
|
106
|
+
current high-water token and a bound Session or the state is corrupt.
|
|
107
|
+
- Capability plane: bearer tokens, host-keyed digests, trust-generation
|
|
108
|
+
binding, revocation, expiry; one opaque `CAPABILITY_REJECTED`
|
|
109
|
+
classification; trust root re-validated per authorization.
|
|
110
|
+
- Transaction discipline: owned BEGIN IMMEDIATE transactions; BUSY /
|
|
111
|
+
ROLLED_BACK / INDETERMINATE classification; refusal over repair; zero-write
|
|
112
|
+
refusals; privacy boundaries (fixed sanitized details).
|
|
113
|
+
|
|
114
|
+
## P2B prerequisites recorded by R1
|
|
115
|
+
|
|
116
|
+
1. Takeover: replace an existing (expired or legacy) Owner under a `takeover`
|
|
117
|
+
capability, allocating the next fencing token and a fresh term; classify
|
|
118
|
+
and pin the takeover races. **Implemented by CE-0002-B3-P2B** — see below.
|
|
119
|
+
2. Detach: define the ordering rule — a current Owner's binding cannot be
|
|
120
|
+
detached while the ownership row references it (Q5) — and the fate of
|
|
121
|
+
Session-bound resume capabilities at detach.
|
|
122
|
+
3. Lease policy location: if TTL policy must outlive a single request (e.g. a
|
|
123
|
+
store-fixed policy), decide whether it belongs in durable metadata.
|
|
124
|
+
|
|
125
|
+
## P2B — Takeover implemented (CE-0002-B3-P2B)
|
|
126
|
+
|
|
127
|
+
Status: **implemented**. The Q1 decision is realized without changing any R1
|
|
128
|
+
boundary: `acquireOwnership` still refuses every existing row; replacement is
|
|
129
|
+
exclusively `takeoverOwnership` under the `takeover` scope.
|
|
130
|
+
|
|
131
|
+
- **Authority.** The `takeover` scope is operational and Session-bound to the
|
|
132
|
+
prospective new Owner Session, consistent with the accepted resume model
|
|
133
|
+
(Q2): issuance requires `sessionId` naming a currently bound Session of the
|
|
134
|
+
Participant, classification compares the durable binding inside the
|
|
135
|
+
mutation's transaction, and revocation requires presenting the bound
|
|
136
|
+
Session. A Session ID alone never grants takeover authority. `send`, `ack`,
|
|
137
|
+
and `read` remain inactive.
|
|
138
|
+
- **Eligibility.** Only an existing EXPIRED row — including a legacy v2 row
|
|
139
|
+
(NULL `lease_started_at`, expired at every instant) — may be taken over. A
|
|
140
|
+
live Owner refuses (`OWNERSHIP_HELD`, evaluated against the persisted term;
|
|
141
|
+
exact expiry is the boundary), a clock-regressed lease refuses
|
|
142
|
+
(`LEASE_CLOCK_REGRESSION`), and an absent row refuses
|
|
143
|
+
(`OWNERSHIP_TAKEOVER_INELIGIBLE`): initial acquisition remains the `attach`
|
|
144
|
+
operation. No force takeover of a live Owner exists in this phase.
|
|
145
|
+
- **Atomicity.** Eligibility, fencing allocation (exactly high-water + 1,
|
|
146
|
+
compare-and-set, `FENCING_EXHAUSTED` at the ceiling), and the Owner
|
|
147
|
+
replacement are one owned IMMEDIATE transaction guarded by the exact
|
|
148
|
+
pre-read values: exactly one winner, no partial fencing allocation, no
|
|
149
|
+
partial replacement, no token reuse, zero-write refusals. A takeover racing
|
|
150
|
+
a renewal of a live lease can never win (the lease is live at every instant
|
|
151
|
+
of the race).
|
|
152
|
+
- **Supersession.** The previous Session immediately loses current ownership:
|
|
153
|
+
its fencing token is stale and its `resume` capability cannot renew,
|
|
154
|
+
release, or verify the new Owner (`OWNERSHIP_NOT_CURRENT` /
|
|
155
|
+
`FENCING_STALE`). Historical Session bindings, Participant identity,
|
|
156
|
+
Endpoint binding, Exchange membership, join boundaries, and audit history
|
|
157
|
+
are untouched; capability rows are never mutated by a takeover.
|
|
158
|
+
- **Recovery.** `reconcileTakeover` is a zero-write, fail-closed
|
|
159
|
+
reconciliation read over the evidence the caller presents (Participant,
|
|
160
|
+
target Session, the exact persisted term boundaries, optional fencing
|
|
161
|
+
token): `COMMITTED` (exact durable match — do not retry), `NO_TRACE` (the
|
|
162
|
+
durable state carries no trace of the presented evidence — a fresh attempt
|
|
163
|
+
is freshly authorized and eligibility-checked), or `EVIDENCE_CONFLICT` (the
|
|
164
|
+
target Session is current under different evidence — operator review, never
|
|
165
|
+
auto-retry). All three are effect-level classifications of durable state,
|
|
166
|
+
never operation-level attribution of one invocation: `COMMITTED` is not an
|
|
167
|
+
exactly-once claim, and `NO_TRACE` is not proof that the original invocation
|
|
168
|
+
never committed (a committed takeover later superseded also reports
|
|
169
|
+
`NO_TRACE`). Documented limitations, deliberately not papered over with
|
|
170
|
+
exactly-once semantics: a refused and a rolled-back takeover leave identical
|
|
171
|
+
durable state; a committed-then-superseded takeover reports `NO_TRACE`; a
|
|
172
|
+
committed retry with identical parameters is the same logical request. No
|
|
173
|
+
background recovery workers.
|
|
174
|
+
- **Evidence availability (clarified, no behavior change).** The operation
|
|
175
|
+
clock is sampled inside the owned transaction (see B3-P2B-R1 below), so the
|
|
176
|
+
sampled instant is not an ordinary caller-supplied request field:
|
|
177
|
+
`leaseStartedAt` is that sample and `expiresAt` is `sample + ttl`. The exact
|
|
178
|
+
term normally reaches the caller only through a successful return, or when
|
|
179
|
+
the caller itself supplies and records the operation clock seam; a
|
|
180
|
+
default-clock caller whose call ended INDETERMINATE does not know it, and
|
|
181
|
+
pre-call wall-clock sampling does not reconstruct it. `reconcileTakeover`
|
|
182
|
+
currently requires `participantId`, `sessionId`, `leaseStartedAt`, and
|
|
183
|
+
`expiresAt`, with `fencingToken` optional, and callers must not fabricate,
|
|
184
|
+
estimate, or round term boundaries to satisfy that shape. INDETERMINATE is
|
|
185
|
+
not ROLLED_BACK: the invalidated connection must not be reused, inspection
|
|
186
|
+
requires a freshly opened store, and a lost acknowledgement never authorizes
|
|
187
|
+
a blind retry. When exact term evidence is unavailable the outcome simply
|
|
188
|
+
stays unknown to the caller — there is no UNKNOWN input and no UNKNOWN
|
|
189
|
+
disposition in the API. A pre-call `readFencingHighWater` snapshot that is
|
|
190
|
+
unchanged on a later valid read of the same Participant shows that no
|
|
191
|
+
fencing allocation committed for that Participant between the two reads; an
|
|
192
|
+
increased high-water identifies no invocation, proves nothing about this
|
|
193
|
+
caller's takeover, and never establishes a durable lease. The operator-only
|
|
194
|
+
recovery caller resolving CE-TAKEOVER-RECOVERY-CALLER-001 deliberately does
|
|
195
|
+
not call `reconcileTakeover`: before writing it requires exact endpoint and
|
|
196
|
+
old-owner evidence and an expired durable lease; it then binds a fresh
|
|
197
|
+
recovery Session, uses the existing Session-bound `takeover` primitive,
|
|
198
|
+
releases that lease, and detaches the recovery Session. A successful caller
|
|
199
|
+
returns a capability-free receipt plus durable fencing/session evidence. Any
|
|
200
|
+
INDETERMINATE stage remains unknown and is never retried automatically.
|
|
201
|
+
`reconcileTakeover` still has no production caller because a default-clock
|
|
202
|
+
caller cannot reconstruct exact term boundaries after losing acknowledgement
|
|
203
|
+
(CE-TAKEOVER-RECONCILIATION-EVIDENCE-001). Full statement:
|
|
204
|
+
`docs/storage-contract.md` §11 "Evidence availability after an uncertain
|
|
205
|
+
outcome".
|
|
206
|
+
- **Legacy ownership.** A migrated v2 row never authorizes and is never
|
|
207
|
+
repaired; it is takeover-eligible, and takeover replaces it with a fully
|
|
208
|
+
persisted v3 term at a fencing token strictly greater than the legacy token.
|
|
209
|
+
- Q4 and Q5 are unchanged: an expired Owner cannot release, and there is no
|
|
210
|
+
detach operation (P2B did not take detach; it remains prerequisite 2).
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## B3-P2B-R1 — transaction-time clock sampling (remediation)
|
|
214
|
+
|
|
215
|
+
Status: **implemented**. Remediates the independently confirmed MAJOR-1 of
|
|
216
|
+
CE-0002-B3-P2B-INDEPENDENT-REVIEW-1 (stale pre-lock clock).
|
|
217
|
+
|
|
218
|
+
**The defect.** P2B sampled each operation's clock *before* `BEGIN
|
|
219
|
+
IMMEDIATE`. An operation that waited for the SQLite writer lock evaluated
|
|
220
|
+
authorization and lease state against the pre-lock instant: a renewal
|
|
221
|
+
initiated before expiry but serialized after it committed from the stale
|
|
222
|
+
sample, reviving an expired lease and locking out a legitimately eligible
|
|
223
|
+
takeover (reproduced by the reviewer with real processes and real lock
|
|
224
|
+
contention). A capability that expired during the lock wait was likewise
|
|
225
|
+
accepted.
|
|
226
|
+
|
|
227
|
+
**The decision (new, contractual):** every owned mutating transaction has
|
|
228
|
+
exactly one authoritative operation-time sampling point — *inside* the owned
|
|
229
|
+
transaction, after `BEGIN IMMEDIATE` has acquired the writer lock:
|
|
230
|
+
`BEGIN IMMEDIATE → sample now → validate authority → evaluate current state
|
|
231
|
+
→ mutate → COMMIT`. All checks within one mutation use this one consistent
|
|
232
|
+
instant. This applies to `bindSession`, `acquireOwnership`, `renewLease`,
|
|
233
|
+
`releaseOwnership`, `verifyOwnership`, `takeoverOwnership` (ownership plane)
|
|
234
|
+
and to `createParticipant` (sponsor authorization), `bindEndpointRef`,
|
|
235
|
+
`issueCapability`, `revokeCapability`, and `joinExchange` (capability/membership
|
|
236
|
+
plane). Lease start and expiry derive from the transaction-time clock;
|
|
237
|
+
capability expiry is evaluated at the transaction-time clock, so a capability
|
|
238
|
+
that expired during a lock wait can never be accepted. Request validation
|
|
239
|
+
(field shape, TTL range, identities) still precedes the transaction and
|
|
240
|
+
performs no state-dependent check.
|
|
241
|
+
|
|
242
|
+
**Preserved semantics.** Exact-expiry behavior is unchanged (`now >=
|
|
243
|
+
expiresAt` is expired); clock-regression refusal is unchanged (a pre-term
|
|
244
|
+
instant refuses `LEASE_CLOCK_REGRESSION`); the injected-clock test seam is
|
|
245
|
+
unchanged (it is simply invoked at the transaction-time sampling point); no
|
|
246
|
+
scheduler and no global clock is introduced. Audit of the remaining storage
|
|
247
|
+
mutation entry points (corrected by CE-0002-B3-P2C-A): B2 Exchange/Message
|
|
248
|
+
creation samples its clock during candidate generation, *before* `BEGIN
|
|
249
|
+
IMMEDIATE`; its timestamps are recorded creation metadata that authorize
|
|
250
|
+
nothing, so the accepted B2 contract is unchanged (its invalid-clock behavior
|
|
251
|
+
remains the accepted precedent). Migration/recovery/initialization
|
|
252
|
+
`appliedAt` ledger timestamps likewise authorize nothing and are unchanged.
|
|
253
|
+
|
|
254
|
+
**Lock-wait semantics (now defined).** Authorization and eligibility are
|
|
255
|
+
decided at the serialization instant, not the initiation instant. A renewal
|
|
256
|
+
initiated before expiry but serialized after it refuses `LEASE_EXPIRED`; a
|
|
257
|
+
takeover initiated before expiry but serialized after it is eligible and may
|
|
258
|
+
win; a takeover serialized while the lease is still live refuses
|
|
259
|
+
`OWNERSHIP_HELD`; a capability that expired during the wait refuses
|
|
260
|
+
`CAPABILITY_REJECTED`. Every such refusal is zero-write and never allocates
|
|
261
|
+
a fencing token.
|
|
262
|
+
|
|
263
|
+
**Remaining wall-clock limitations (unchanged, no new monotonicity claim).**
|
|
264
|
+
A wall-clock rollback that lands *inside* the current term remains
|
|
265
|
+
indistinguishable from a valid instant without a persisted monotonic
|
|
266
|
+
observation (Q3); transaction-time sampling decides each operation at its
|
|
267
|
+
own serialization instant and does not create a cross-transaction monotonic
|
|
268
|
+
clock. Refusals remain zero-write.
|
|
269
|
+
|
|
270
|
+
**Regression pins.** `test/storage/lease-lock-wait.test.ts` exercises the
|
|
271
|
+
renewal/takeover/capability lock-wait boundaries with real OS processes, a
|
|
272
|
+
real writer lock, observable initiation markers, and deadline waits against
|
|
273
|
+
durable expiries (no sleep-only assertions, no fake-clock simulation of
|
|
274
|
+
contention).
|
|
275
|
+
## P2C prerequisites recorded by P2B
|
|
276
|
+
|
|
277
|
+
1. Detach semantics (Q5 ordering rule and the fate of Session-bound
|
|
278
|
+
capabilities at detach).
|
|
279
|
+
2. Lease policy location (prerequisite 3 above, unchanged).
|
|
280
|
+
3. `send`/`ack`/`read` scopes remain for the delivery phases.
|
|
281
|
+
|
|
282
|
+
## B3-P2C-A — Irreversible Session lifecycle and schema v4 (CE-0002-B3-P2C-A)
|
|
283
|
+
|
|
284
|
+
Status: **implemented** (structural foundation only). Realizes the accepted
|
|
285
|
+
CE-0002-B3-P2C-D0 design decisions D1, D2, D5 (as a schema guard), D9 (read),
|
|
286
|
+
D14, and D15. **No detach operation exists yet**; capability scope semantics
|
|
287
|
+
are unchanged (no `detach` scope; `resume` is not yet widened to
|
|
288
|
+
self-detach). P3 is not started.
|
|
289
|
+
|
|
290
|
+
- **States.** A Session row is exactly ATTACHED (`detached_at IS NULL`,
|
|
291
|
+
`private_revision = 1`) or DETACHED (canonical `detached_at` not earlier
|
|
292
|
+
than `bound_at`, `private_revision = 2`). Every other combination is
|
|
293
|
+
`STORAGE_OWNERSHIP_CORRUPT`: refused, zero-write, never repaired. DETACHED
|
|
294
|
+
is terminal; a detached Session is never a current binding and never
|
|
295
|
+
regains authority.
|
|
296
|
+
- **Schema v4 (triggers only).** Session rows are born ATTACHED, never
|
|
297
|
+
deleted, and keep immutable Participant ID, Session ID, and `bound_at`; the
|
|
298
|
+
only permitted UPDATE is the one-way ATTACHED r1 → DETACHED r2 transition;
|
|
299
|
+
the current Owner Session cannot be detached; an ownership row can never
|
|
300
|
+
reference a detached Session. Session IDs are therefore never reused. No
|
|
301
|
+
table rebuild, no new column, no new capability scope.
|
|
302
|
+
R1: the transition also requires a canonical `detached_at` no earlier than
|
|
303
|
+
a canonical `bound_at`, enforced by SQLite itself — see
|
|
304
|
+
`docs/storage-contract.md` §12 for the exact SQLite-versus-application
|
|
305
|
+
validation boundary, the `recursive_triggers` requirement, and the
|
|
306
|
+
privileged-writer trust limits.
|
|
307
|
+
- **Ownership interaction (Q5 formalized).** The current Owner — live,
|
|
308
|
+
expired, legacy, or clock-regressed — cannot be detached while the
|
|
309
|
+
ownership row references it; the way out is the existing release or
|
|
310
|
+
takeover. Detach never deletes or replaces ownership and never touches the
|
|
311
|
+
fencing high-water.
|
|
312
|
+
- **Validation.** `issueCapability` (Session-bound scopes), `bindSession`,
|
|
313
|
+
`acquireOwnership`, `takeoverOwnership`, `renewLease`, `releaseOwnership`,
|
|
314
|
+
`verifyOwnership`, and every Session read strictly validate the row: a
|
|
315
|
+
DETACHED Session refuses `SESSION_NOT_BOUND`; a malformed row refuses
|
|
316
|
+
`STORAGE_OWNERSHIP_CORRUPT`; re-binding a detached Session ID refuses as
|
|
317
|
+
before (`SESSION_ALREADY_BOUND` / `SESSION_BOUND_TO_OTHER_PARTICIPANT`).
|
|
318
|
+
- **Lifecycle read.** `readSessionLifecycle` is a private, host-local,
|
|
319
|
+
zero-write read over one snapshot returning `ATTACHED` / `DETACHED` /
|
|
320
|
+
`NOT_FOUND` (a foreign pair is `NOT_FOUND`); it validates the
|
|
321
|
+
Participant's ownership state in the same snapshot and authorizes nothing.
|
|
322
|
+
- **Remaining P2C work (P2C-B): implemented** by CE-0002-B3-P2C-B — see the
|
|
323
|
+
next section. P3 (send/ack/read, delivery) is not started.
|
|
324
|
+
|
|
325
|
+
## B3-P2C-B — Authorized Session Detach (CE-0002-B3-P2C-B)
|
|
326
|
+
|
|
327
|
+
Status: **implemented**. Realizes the accepted P2C-D0 design decisions D4,
|
|
328
|
+
D7, D10, and D11 without changing any accepted boundary: schema v4 stays
|
|
329
|
+
frozen (no DDL, no new migration, no checksum change), no `detach` capability
|
|
330
|
+
scope exists, and Disconnect is not Detach — Detach is a durable binding
|
|
331
|
+
lifecycle fact of the private storage plane, never a transport event.
|
|
332
|
+
|
|
333
|
+
- **Operation (D4).** `detachSession(store, request)` performs the one-way
|
|
334
|
+
ATTACHED → DETACHED transition under the Session's own Session-bound
|
|
335
|
+
`resume` capability — self only. The capability must authorize exactly its
|
|
336
|
+
bound Session; a Session ID alone is never a credential. Inside one owned
|
|
337
|
+
BEGIN IMMEDIATE transaction, in order: transaction-time clock (sampled
|
|
338
|
+
after the writer lock is acquired), capability validation (trust
|
|
339
|
+
generation, expiry, revocation, Session binding), Participant identity,
|
|
340
|
+
Session binding, complete ownership-state validation, the
|
|
341
|
+
current-Owner refusal, the clock-regression refusal, and the guarded
|
|
342
|
+
`detached_at IS NULL AND private_revision = 1` → `detached_at = now,`
|
|
343
|
+
`private_revision = 2` update with exactly one changed row.
|
|
344
|
+
- **Ownership interaction (Q5).** The current Owner — live, expired, legacy
|
|
345
|
+
(NULL `lease_started_at`), or clock-regressed — refuses
|
|
346
|
+
`SESSION_IS_CURRENT_OWNER` while the ownership row references its Session,
|
|
347
|
+
regardless of lease expiry. The way out is the existing release (live
|
|
348
|
+
Owners only; Q4 preserved: an expired Owner cannot release) or an
|
|
349
|
+
authorized takeover, after which the superseded former Owner detaches
|
|
350
|
+
freely. Detach never modifies the ownership row, never touches the fencing
|
|
351
|
+
high-water, never deletes the Session row, and never mutates capability
|
|
352
|
+
rows. Concurrent release / takeover / renew outcomes are derived from the
|
|
353
|
+
serialized durable state; the real-process lock-wait scenarios F and G pin
|
|
354
|
+
the serialized boundaries.
|
|
355
|
+
- **Capability lifecycle (D7).** Session lifecycle validation is centralized
|
|
356
|
+
in capability classification: a Session-bound capability whose bound
|
|
357
|
+
Session is DETACHED is unusable for every presenter. The persisted
|
|
358
|
+
`detached_at` is the lifecycle source of truth — read-time invalidation
|
|
359
|
+
only, no bulk revocation, no capability-row rewrites. Caller-visible
|
|
360
|
+
refusals remain the one opaque `CAPABILITY_REJECTED`; the host-local
|
|
361
|
+
status read identifies `SESSION_DETACHED`. The future Session-bound
|
|
362
|
+
scopes (`send`, `ack`, `read` — not implemented in this phase) inherit the
|
|
363
|
+
check unchanged when they join `SESSION_BOUND_CAPABILITY_SCOPES`. A
|
|
364
|
+
Session-bound capability whose binding row is missing or malformed refuses
|
|
365
|
+
`STORAGE_OWNERSHIP_CORRUPT` — never adopted, never repaired. Deliberate
|
|
366
|
+
error-precedence change (documented, pinned): for a DETACHED Session, the
|
|
367
|
+
capability-plane refusal now outranks the operation-level
|
|
368
|
+
`SESSION_NOT_BOUND` that `renewLease` / `releaseOwnership` /
|
|
369
|
+
`verifyOwnership` / `takeoverOwnership` previously surfaced; `attach`
|
|
370
|
+
(not Session-bound) still refuses `SESSION_NOT_BOUND` at the operation.
|
|
371
|
+
Revoking a detached Session's capability refuses exactly like revoking an
|
|
372
|
+
expired one.
|
|
373
|
+
- **Recovery.** `reconcileDetach(store, evidence)` is the private, read-based,
|
|
374
|
+
zero-write, fail-closed reconciliation after an uncertain outcome (failure
|
|
375
|
+
before or after COMMIT, lost response, BUSY, ROLLED_BACK, INDETERMINATE):
|
|
376
|
+
COMMITTED / NO_TRACE / EVIDENCE_CONFLICT classified from the durable
|
|
377
|
+
Session state in one snapshot, with ownership coherence validated in the
|
|
378
|
+
same snapshot (concurrent takeovers and releases are simply part of the
|
|
379
|
+
observed state). The evidence `detachedAt` is optional: with it, an exact
|
|
380
|
+
match is COMMITTED and any mismatch is EVIDENCE_CONFLICT (operator
|
|
381
|
+
review); without it, a DETACHED row is COMMITTED because the terminal
|
|
382
|
+
durable effect exists. The durable effect is idempotent; the API response
|
|
383
|
+
is not — a retry of a committed detach refuses `CAPABILITY_REJECTED` — so
|
|
384
|
+
the discipline is reconcile first, never blindly retry, and a fresh
|
|
385
|
+
attempt after NO_TRACE is a new full authorization, not a replay. No
|
|
386
|
+
exactly-once execution is claimed. `reconcileTakeover` is unchanged.
|
|
387
|
+
- **Lease policy (D10/D11).** The host owns lease policy: default TTL 120 s,
|
|
388
|
+
accepted range 30–3600 s, renewal recommendation 30 s. An adapter may
|
|
389
|
+
REQUEST a TTL per operation (`leaseTtlSeconds`); the request only computes
|
|
390
|
+
the new expiry at acquisition or renewal and never reinterprets a stored
|
|
391
|
+
term. No policy is persisted — there is no policy table and none is
|
|
392
|
+
planned for this phase. Hosts should schedule renewal well before expiry
|
|
393
|
+
using the `expiresAt` returned by acquisition and renewal (and re-derive
|
|
394
|
+
the schedule from each renewal's returned `expiresAt`), because an
|
|
395
|
+
operation serialized after expiry refuses `LEASE_EXPIRED` and cannot
|
|
396
|
+
revive the term; the persisted `lease_started_at` / `expires_at` boundaries
|
|
397
|
+
always decide.
|
|
398
|
+
|
|
399
|
+
## B3-P3A — Scoped current-Owner authority
|
|
400
|
+
|
|
401
|
+
Status: **implemented for private storage send/read only**. The historical P2
|
|
402
|
+
sections above remain the record of their phase boundaries; where they say
|
|
403
|
+
`send`/`read` were inactive, this P3A amendment supersedes that status.
|
|
404
|
+
|
|
405
|
+
`send` and `read` are now operational and are Session-bound exactly like
|
|
406
|
+
`resume`/`takeover`: issuance requires a durable ATTACHED Session binding,
|
|
407
|
+
classification binds Participant + exact scope + trust generation + Session,
|
|
408
|
+
revocation presents that Session, and terminal detach makes the token unusable
|
|
409
|
+
without rewriting capability rows. `ack` remains inactive.
|
|
410
|
+
|
|
411
|
+
The shared current-Owner primitive defaults to `resume` for all accepted P2
|
|
412
|
+
renew/release/verification callers, preserving their API and refusal precedence.
|
|
413
|
+
P3A callers explicitly request only `send` or `read`; neither a resume token nor
|
|
414
|
+
a takeover token authorizes those operations. Inside the caller-owned IMMEDIATE
|
|
415
|
+
transaction, capability classification precedes Participant/Session checks, then
|
|
416
|
+
the durable ownership row establishes the current Owner, the presented fencing
|
|
417
|
+
token must equal the persisted monotonic token/high-water state, and the
|
|
418
|
+
persisted lease term must be active without clock regression.
|
|
419
|
+
|
|
420
|
+
P3A samples authority time only after the writer lock is acquired. A send racing
|
|
421
|
+
release serializes either before release and may commit, or after release and
|
|
422
|
+
refuses `OWNERSHIP_NOT_CURRENT`. A near-expiry send racing takeover similarly
|
|
423
|
+
commits only before replacement or refuses as the former Owner afterward; it
|
|
424
|
+
cannot commit under stale fencing. Independent-process tests pin both outcomes.
|
|
425
|
+
|
|
426
|
+
This amendment does not broaden takeover recovery, define Pi Session lifecycle
|
|
427
|
+
outside the existing durable binding, or grant authority from a Session ID,
|
|
428
|
+
Participant ID, Worker/Advisor label, endpoint label, or fencing token alone.
|
|
429
|
+
ACK and external delivery remain outstanding P3 work.
|