@creatornader/agent-bridge 0.2.0 → 0.3.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 +63 -1
- package/README.md +467 -29
- package/ROADMAP.md +104 -0
- package/SECURITY.md +73 -0
- package/SKILL.md +68 -2
- package/bin/agent-bridge +2 -2
- package/clients/claude-desktop.json +3 -1
- package/dist/archive.d.ts +163 -0
- package/dist/archive.js +50 -0
- package/dist/archive.js.map +1 -0
- package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
- package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
- package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
- package/dist/bridge.d.ts +76 -17
- package/dist/bridge.js +52 -24
- package/dist/chunk-2DFNOLMO.js +618 -0
- package/dist/chunk-2DFNOLMO.js.map +1 -0
- package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
- package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
- package/dist/chunk-45BZGB77.js +709 -0
- package/dist/chunk-45BZGB77.js.map +1 -0
- package/dist/chunk-4VB44KJU.js +230 -0
- package/dist/chunk-4VB44KJU.js.map +1 -0
- package/dist/chunk-5GMB3A77.js +1013 -0
- package/dist/chunk-5GMB3A77.js.map +1 -0
- package/dist/chunk-A6W4QALF.js +1258 -0
- package/dist/chunk-A6W4QALF.js.map +1 -0
- package/dist/chunk-BZLMF5O5.js +328 -0
- package/dist/chunk-BZLMF5O5.js.map +1 -0
- package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
- package/dist/chunk-CEXBMOI5.js.map +1 -0
- package/dist/chunk-GADH244D.js +527 -0
- package/dist/chunk-GADH244D.js.map +1 -0
- package/dist/chunk-MBPZXTJA.js +2428 -0
- package/dist/chunk-MBPZXTJA.js.map +1 -0
- package/dist/chunk-NCO7RLBM.js +146 -0
- package/dist/chunk-NCO7RLBM.js.map +1 -0
- package/dist/chunk-Q6PPMXJC.js +1050 -0
- package/dist/chunk-Q6PPMXJC.js.map +1 -0
- package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
- package/dist/chunk-SPSMZOCJ.js.map +1 -0
- package/dist/chunk-UAHMP6GC.js +54 -0
- package/dist/chunk-UAHMP6GC.js.map +1 -0
- package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
- package/dist/chunk-VOQIDTCJ.js.map +1 -0
- package/dist/chunk-WAI2IYIQ.js +554 -0
- package/dist/chunk-WAI2IYIQ.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +3577 -250
- package/dist/cli.js.map +1 -1
- package/dist/client-config.d.ts +1 -1
- package/dist/client-config.js +1 -1
- package/dist/client-runtime.d.ts +10 -6
- package/dist/client-runtime.js +9 -6
- package/dist/contracts/generate.d.ts +2 -0
- package/dist/contracts/generate.js +247 -0
- package/dist/contracts/generate.js.map +1 -0
- package/dist/dr.d.ts +324 -0
- package/dist/dr.js +124 -0
- package/dist/dr.js.map +1 -0
- package/dist/gateway-CggQ8vTC.d.ts +78 -0
- package/dist/gateway-main.js +190 -8
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +4 -3
- package/dist/gateway.js +4 -3
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +13 -8
- package/dist/sqlite.d.ts +94 -12
- package/dist/sqlite.js +7 -4
- package/docs/architecture-v2.md +375 -0
- package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
- package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
- package/docs/ecosystem.md +54 -0
- package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
- package/docs/troubleshooting.md +89 -0
- package/llms.txt +96 -4
- package/openapi/agent-bridge-v2.openapi.json +9610 -0
- package/package.json +24 -4
- package/schemas/agent-bridge-v2.capabilities.json +1829 -0
- package/schemas/agent-bridge-v2.mcp.json +2074 -0
- package/schemas/agent-bridge-v2.schema.json +4560 -0
- package/sql/migrations/008_message_projects.sql +36 -0
- package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
- package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
- package/sql/migrations/011_credential_security.sql +875 -0
- package/sql/migrations/012_request_authority.sql +177 -0
- package/sql/migrations/013_row_isolation.sql +495 -0
- package/sql/migrations/014_owner_control_plane.sql +1641 -0
- package/sql/migrations/015_portable_archives.sql +1813 -0
- package/sql/migrations/016_native_dr_fence.sql +1005 -0
- package/bin/CLAUDE.md +0 -11
- package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
- package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
- package/dist/chunk-4KAOBC5X.js.map +0 -1
- package/dist/chunk-6MAZ6YLZ.js +0 -146
- package/dist/chunk-6MAZ6YLZ.js.map +0 -1
- package/dist/chunk-AXL2I7FH.js.map +0 -1
- package/dist/chunk-BV6W4AJK.js +0 -211
- package/dist/chunk-BV6W4AJK.js.map +0 -1
- package/dist/chunk-CKSKUBDQ.js +0 -153
- package/dist/chunk-CKSKUBDQ.js.map +0 -1
- package/dist/chunk-JOUBDRYX.js +0 -272
- package/dist/chunk-JOUBDRYX.js.map +0 -1
- package/dist/chunk-JTC5YURG.js +0 -49
- package/dist/chunk-JTC5YURG.js.map +0 -1
- package/dist/chunk-O26W5QHS.js +0 -409
- package/dist/chunk-O26W5QHS.js.map +0 -1
- package/dist/chunk-WEULPMUY.js +0 -988
- package/dist/chunk-WEULPMUY.js.map +0 -1
- package/dist/chunk-YD6YJATN.js.map +0 -1
- package/dist/gateway-BULozv3K.d.ts +0 -54
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as BridgeMessage, c as BridgePrincipal, B as BridgeDelivery,
|
|
2
|
-
import {
|
|
1
|
+
import { b as BridgeMessage, c as BridgePrincipal, B as BridgeDelivery, g as DeliveryState, R as RetryPolicy, A as AgentPresence, a as BridgeDeliveryEvent } from './bridge-domain-CBX3IVqC.js';
|
|
2
|
+
import { d as MessageQuery, M as MessagePage, B as BridgeDiagnostics, I as InsertMessageResult, a as BridgeStore, C as ClaimOptions, c as DeliveryQuery, b as DeliveryPage, D as DeliveryEventPage } from './bridge-store-DPVSCwfs.js';
|
|
3
3
|
|
|
4
4
|
type PendingMessage = Omit<BridgeMessage, "sequence" | "createdAt">;
|
|
5
5
|
interface EdgeScope {
|
|
@@ -20,11 +20,24 @@ interface EdgeEnqueueResult {
|
|
|
20
20
|
}
|
|
21
21
|
interface EdgeStats {
|
|
22
22
|
pending: number;
|
|
23
|
+
due: number;
|
|
24
|
+
scheduled: number;
|
|
25
|
+
leased: number;
|
|
23
26
|
blocked: number;
|
|
24
27
|
cached: number;
|
|
25
28
|
pullCursor?: string;
|
|
26
29
|
lastSyncAt?: string;
|
|
27
30
|
lastError?: string;
|
|
31
|
+
oldestDueAt?: string;
|
|
32
|
+
queueLagMs: number;
|
|
33
|
+
oldestBlockedAt?: string;
|
|
34
|
+
blockedAgeMs: number | null;
|
|
35
|
+
blockedMaxAttempts: number;
|
|
36
|
+
blockedLastError?: string;
|
|
37
|
+
nextRetryAt?: string;
|
|
38
|
+
lastOutboundSyncAt?: string;
|
|
39
|
+
lastInboundSyncAt?: string;
|
|
40
|
+
lastAttemptAt?: string;
|
|
28
41
|
}
|
|
29
42
|
declare class EdgeConflictError extends Error {
|
|
30
43
|
readonly code = "edge_idempotency_conflict";
|
|
@@ -37,25 +50,30 @@ declare class SQLiteEdgeStore {
|
|
|
37
50
|
private readonly scope;
|
|
38
51
|
private readonly busyTimeoutMs;
|
|
39
52
|
private readonly db;
|
|
53
|
+
private readonly databasePath;
|
|
54
|
+
private readonly preexistingFiles;
|
|
40
55
|
private readonly key;
|
|
41
56
|
private initialized;
|
|
57
|
+
private initialization?;
|
|
42
58
|
constructor(path: string, scope: EdgeScope, busyTimeoutMs?: number);
|
|
43
59
|
private restrictFiles;
|
|
44
60
|
get scopeKey(): string;
|
|
45
61
|
initialize(): Promise<void>;
|
|
62
|
+
private initializeOnce;
|
|
46
63
|
private ready;
|
|
47
64
|
enqueue(input: PendingMessage, now?: Date): Promise<EdgeEnqueueResult>;
|
|
48
65
|
claimNext(now?: Date, leaseMs?: number): Promise<EdgeOutboxRecord | undefined>;
|
|
49
66
|
retry(record: EdgeOutboxRecord, error: string, availableAt: Date): Promise<void>;
|
|
50
|
-
block(record: EdgeOutboxRecord, error: string): Promise<void>;
|
|
51
|
-
commit(record: EdgeOutboxRecord, message: BridgeMessage,
|
|
67
|
+
block(record: EdgeOutboxRecord, error: string, now?: Date): Promise<void>;
|
|
68
|
+
commit(record: EdgeOutboxRecord, message: BridgeMessage, now?: Date): Promise<void>;
|
|
52
69
|
private cacheOne;
|
|
53
70
|
cachePage(messages: BridgeMessage[], cursor: string | undefined, now?: Date): Promise<void>;
|
|
54
71
|
cacheLatest(messages: BridgeMessage[], now?: Date): Promise<void>;
|
|
55
72
|
list(query?: MessageQuery): Promise<MessagePage>;
|
|
56
73
|
pullCursor(): Promise<string | undefined>;
|
|
57
74
|
noteError(error: string): Promise<void>;
|
|
58
|
-
|
|
75
|
+
noteAttempt(now?: Date): Promise<void>;
|
|
76
|
+
stats(now?: Date): Promise<EdgeStats>;
|
|
59
77
|
close(): Promise<void>;
|
|
60
78
|
}
|
|
61
79
|
|
|
@@ -66,6 +84,8 @@ interface SyncInsertResult extends InsertMessageResult {
|
|
|
66
84
|
interface CachedMessagePage extends MessagePage {
|
|
67
85
|
source: "remote" | "cache";
|
|
68
86
|
stale: boolean;
|
|
87
|
+
degraded: boolean;
|
|
88
|
+
acknowledgements: "authoritative" | "unknown";
|
|
69
89
|
lastSyncedAt?: string;
|
|
70
90
|
}
|
|
71
91
|
interface SyncReport {
|
|
@@ -82,12 +102,28 @@ interface SyncReport {
|
|
|
82
102
|
failureRetryable?: boolean;
|
|
83
103
|
}
|
|
84
104
|
interface SyncDiagnostics extends BridgeDiagnostics {
|
|
85
|
-
remoteReachable: boolean;
|
|
105
|
+
remoteReachable: boolean | null;
|
|
86
106
|
outboxPending: number;
|
|
87
107
|
outboxBlocked: number;
|
|
88
108
|
cachedMessages: number;
|
|
89
109
|
lastSyncAt?: string;
|
|
90
110
|
lastSyncError?: string;
|
|
111
|
+
outboxDue: number;
|
|
112
|
+
outboxScheduled: number;
|
|
113
|
+
outboxLeased: number;
|
|
114
|
+
outboxOldestDueAt?: string;
|
|
115
|
+
outboxQueueLagMs: number;
|
|
116
|
+
outboxOldestBlockedAt?: string;
|
|
117
|
+
outboxBlockedAgeMs: number | null;
|
|
118
|
+
outboxBlockedMaxAttempts: number;
|
|
119
|
+
outboxBlockedLastError?: string;
|
|
120
|
+
outboxNextRetryAt?: string;
|
|
121
|
+
lastOutboundSyncAt?: string;
|
|
122
|
+
lastInboundSyncAt?: string;
|
|
123
|
+
lastSyncAttemptAt?: string;
|
|
124
|
+
syncLoopState: "disabled" | "idle" | "running" | "backoff" | "stopped" | "failed";
|
|
125
|
+
syncLoopError?: string;
|
|
126
|
+
remoteError?: string;
|
|
91
127
|
}
|
|
92
128
|
interface SyncingBridgeStoreOptions {
|
|
93
129
|
now?: () => Date;
|
|
@@ -95,6 +131,8 @@ interface SyncingBridgeStoreOptions {
|
|
|
95
131
|
baseDelayMs?: number;
|
|
96
132
|
maxDelayMs?: number;
|
|
97
133
|
leaseMs?: number;
|
|
134
|
+
autoSync?: boolean;
|
|
135
|
+
idleDelayMs?: number;
|
|
98
136
|
}
|
|
99
137
|
declare function isRetryableSyncError(error: unknown): boolean;
|
|
100
138
|
declare class SyncingBridgeStore implements BridgeStore {
|
|
@@ -106,26 +144,54 @@ declare class SyncingBridgeStore implements BridgeStore {
|
|
|
106
144
|
private readonly baseDelayMs;
|
|
107
145
|
private readonly maxDelayMs;
|
|
108
146
|
private readonly leaseMs;
|
|
147
|
+
private readonly autoSync;
|
|
148
|
+
private readonly idleDelayMs;
|
|
109
149
|
private remoteReady;
|
|
150
|
+
private stopped;
|
|
151
|
+
private wakeTimer;
|
|
152
|
+
private wake;
|
|
153
|
+
private background;
|
|
154
|
+
private readonly closeController;
|
|
155
|
+
private backgroundError;
|
|
156
|
+
private wakeRequested;
|
|
157
|
+
private remoteReachable;
|
|
158
|
+
private loopState;
|
|
110
159
|
constructor(edge: SQLiteEdgeStore, remote: BridgeStore, principal: BridgePrincipal, options?: SyncingBridgeStoreOptions);
|
|
111
160
|
initialize(): Promise<void>;
|
|
161
|
+
private startTransportLoop;
|
|
162
|
+
private wait;
|
|
163
|
+
private wakeTransport;
|
|
164
|
+
private fatal;
|
|
165
|
+
private assertHealthy;
|
|
166
|
+
private transportLoop;
|
|
112
167
|
verifyRemote(): Promise<void>;
|
|
113
168
|
private ensureRemote;
|
|
114
169
|
private assertPrincipal;
|
|
115
170
|
private delay;
|
|
171
|
+
private cancelled;
|
|
116
172
|
private flushOne;
|
|
117
173
|
insertMessage(message: PendingMessage): Promise<SyncInsertResult>;
|
|
118
174
|
private pull;
|
|
119
175
|
sync(options?: {
|
|
120
176
|
maxPush?: number;
|
|
121
177
|
maxPages?: number;
|
|
178
|
+
signal?: AbortSignal;
|
|
122
179
|
}): Promise<SyncReport>;
|
|
123
180
|
listMessages(principal: BridgePrincipal, query?: MessageQuery): Promise<CachedMessagePage>;
|
|
124
|
-
recordReceipt(
|
|
181
|
+
recordReceipt(principal: BridgePrincipal, messageIds: string[], readAt?: Date): Promise<number>;
|
|
125
182
|
claimDelivery(principal: BridgePrincipal, options: ClaimOptions): Promise<BridgeDelivery | null>;
|
|
126
183
|
renewDelivery(principal: BridgePrincipal, deliveryId: string, leaseToken: string, leaseMs: number): Promise<BridgeDelivery | null>;
|
|
127
|
-
settleDelivery(principal: BridgePrincipal, deliveryId: string, leaseToken: string, state: Extract<DeliveryState, "acked" | "retrying" | "dead">, error
|
|
128
|
-
|
|
184
|
+
settleDelivery(principal: BridgePrincipal, deliveryId: string, leaseToken: string, state: Extract<DeliveryState, "acked" | "retrying" | "dead">, error?: string, _retryPolicy?: RetryPolicy): Promise<BridgeDelivery | null>;
|
|
185
|
+
listDeliveries(principal: BridgePrincipal, query?: DeliveryQuery): Promise<DeliveryPage>;
|
|
186
|
+
listDeliveryEvents(principal: BridgePrincipal, id: string, query?: {
|
|
187
|
+
cursor?: string;
|
|
188
|
+
limit?: number;
|
|
189
|
+
}): Promise<DeliveryEventPage>;
|
|
190
|
+
cancelDelivery(principal: BridgePrincipal, id: string): Promise<BridgeDelivery | null>;
|
|
191
|
+
requeueDelivery(principal: BridgePrincipal, id: string): Promise<BridgeDelivery | null>;
|
|
192
|
+
diagnostics(principal: BridgePrincipal, options?: {
|
|
193
|
+
mode?: "snapshot" | "probe";
|
|
194
|
+
}): Promise<SyncDiagnostics>;
|
|
129
195
|
heartbeat(principal: BridgePrincipal, leaseMs: number, runtimeType?: string, capabilities?: string[]): Promise<AgentPresence>;
|
|
130
196
|
listPresence(principal: BridgePrincipal): Promise<AgentPresence[]>;
|
|
131
197
|
close(): Promise<void>;
|
|
@@ -135,21 +201,37 @@ declare class SQLiteBridgeStore implements BridgeStore {
|
|
|
135
201
|
private readonly path;
|
|
136
202
|
private readonly busyTimeoutMs;
|
|
137
203
|
private readonly db;
|
|
204
|
+
private readonly databasePath;
|
|
205
|
+
private readonly preexistingFiles;
|
|
138
206
|
private initialized;
|
|
207
|
+
private initialization?;
|
|
139
208
|
constructor(path?: string, busyTimeoutMs?: number);
|
|
140
209
|
private restrictFiles;
|
|
141
210
|
initialize(): Promise<void>;
|
|
211
|
+
private initializeOnce;
|
|
142
212
|
private ready;
|
|
143
213
|
insertMessage(input: Omit<BridgeMessage, "sequence" | "createdAt">): Promise<InsertMessageResult>;
|
|
144
214
|
listMessages(principal: BridgePrincipal, query?: MessageQuery): Promise<MessagePage>;
|
|
145
|
-
recordReceipt(
|
|
215
|
+
recordReceipt(principal: BridgePrincipal, messageIds: string[], readAt?: Date): Promise<number>;
|
|
146
216
|
claimDelivery(principal: BridgePrincipal, options: ClaimOptions): Promise<BridgeDelivery | null>;
|
|
147
217
|
renewDelivery(principal: BridgePrincipal, id: string, token: string, leaseMs: number): Promise<BridgeDelivery | null>;
|
|
148
|
-
settleDelivery(principal: BridgePrincipal, id: string, token: string, state: "acked" | "retrying" | "dead", error
|
|
218
|
+
settleDelivery(principal: BridgePrincipal, id: string, token: string, state: "acked" | "retrying" | "dead", error?: string, _retryPolicy?: RetryPolicy): Promise<BridgeDelivery | null>;
|
|
149
219
|
diagnostics(principal: BridgePrincipal): Promise<BridgeDiagnostics>;
|
|
150
220
|
heartbeat(principal: BridgePrincipal, leaseMs: number, runtimeType?: string, capabilities?: string[]): Promise<AgentPresence>;
|
|
151
221
|
listPresence(principal: BridgePrincipal): Promise<AgentPresence[]>;
|
|
152
|
-
|
|
222
|
+
listDeliveries(principal: BridgePrincipal, query?: DeliveryQuery): Promise<{
|
|
223
|
+
deliveries: BridgeDelivery[];
|
|
224
|
+
cursor: string | undefined;
|
|
225
|
+
}>;
|
|
226
|
+
listDeliveryEvents(principal: BridgePrincipal, deliveryId: string, query?: {
|
|
227
|
+
cursor?: string;
|
|
228
|
+
limit?: number;
|
|
229
|
+
}): Promise<{
|
|
230
|
+
events: BridgeDeliveryEvent[];
|
|
231
|
+
cursor: string | undefined;
|
|
232
|
+
}>;
|
|
233
|
+
cancelDelivery(principal: BridgePrincipal, id: string): Promise<BridgeDelivery | null>;
|
|
234
|
+
requeueDelivery(principal: BridgePrincipal, id: string): Promise<BridgeDelivery | null>;
|
|
153
235
|
close(): Promise<void>;
|
|
154
236
|
}
|
|
155
237
|
|
package/dist/sqlite.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
EdgeConflictError,
|
|
4
|
-
SQLiteBridgeStore,
|
|
5
4
|
SQLiteEdgeStore,
|
|
6
5
|
SyncingBridgeStore,
|
|
7
6
|
edgeMessageFingerprint,
|
|
8
7
|
edgeScopeKey,
|
|
9
8
|
isRetryableSyncError,
|
|
10
9
|
stableIdempotency
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import
|
|
13
|
-
|
|
10
|
+
} from "./chunk-Q6PPMXJC.js";
|
|
11
|
+
import {
|
|
12
|
+
SQLiteBridgeStore
|
|
13
|
+
} from "./chunk-5GMB3A77.js";
|
|
14
|
+
import "./chunk-NCO7RLBM.js";
|
|
15
|
+
import "./chunk-VOQIDTCJ.js";
|
|
16
|
+
import "./chunk-BZLMF5O5.js";
|
|
14
17
|
export {
|
|
15
18
|
EdgeConflictError,
|
|
16
19
|
SQLiteBridgeStore,
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Agent Bridge v2 architecture
|
|
2
|
+
|
|
3
|
+
Status: living architecture for the 0.3.0 development line. Version 0.2.0 was released on July 14, 2026.
|
|
4
|
+
|
|
5
|
+
## Product boundary
|
|
6
|
+
|
|
7
|
+
Agent Bridge is the durable, pull-first mailbox and work-delivery control plane for agents that run in different clients, processes, sessions, and machines. It supports three operating modes: local SQLite, the authenticated PostgreSQL gateway, and the legacy Supabase adapter.
|
|
8
|
+
|
|
9
|
+
History visibility is caller-relative. `inbox` is the default and preserves broadcast-plus-targeted visibility; `sent` is source equal to the caller; `all` is their union. Receipt state (`any`, `unread`, `read`) is valid only for inbox and is always evaluated for the authenticated caller. Opaque v2 cursors bind workspace, caller, mailbox, and normalized filters. Readers temporarily accept v1 sequence cursors but emit only v2. After an edge cache contract upgrade, the gateway resets the authoritative pull cursor and replays `all` visibility; the publication outbox is never treated as sent history.
|
|
10
|
+
|
|
11
|
+
These authorization guarantees apply to local v2 and the authenticated gateway. The legacy Supabase adapter can enforce them only cooperatively because its publishable key can call the underlying PostgREST table and receipt RPC directly.
|
|
12
|
+
|
|
13
|
+
- Local mode runs without an account or network connection.
|
|
14
|
+
- Shared mode uses a remote service so agents on different machines see the same history and delivery state.
|
|
15
|
+
|
|
16
|
+
The protocol must support informational context and executable work without treating them as the same thing. A2A and application task semantics sit above Agent Bridge. MCP, CLI, and HTTPS are access surfaces. Optional transports may sit below the core, but they cannot replace authoritative cursor replay or durable delivery state.
|
|
17
|
+
|
|
18
|
+
[ADR-0001](decisions/0001-protocol-layers-and-acknowledgment-semantics.md) defines these layers and the distinct meanings of receipts, claims, leases, delivery settlement, and external task completion.
|
|
19
|
+
|
|
20
|
+
[ADR-0002](decisions/0002-canonical-operation-contract-registry.md) defines the canonical cross-surface operation contract, deterministic artifacts, capability discovery, and protocol negotiation.
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### PostgreSQL is the shared source of truth
|
|
25
|
+
|
|
26
|
+
PostgreSQL stores shared messages, identities, receipts, deliveries, and migration state. Supabase remains a supported managed PostgreSQL provider, but clients do not depend on Supabase-specific behavior.
|
|
27
|
+
|
|
28
|
+
A standalone Agent Bridge API sits between remote clients and PostgreSQL. Clients authenticate to the API with scoped credentials. They do not receive a database password or rely on a shared Supabase publishable key for identity.
|
|
29
|
+
|
|
30
|
+
Supabase can still host the database. Existing direct PostgREST deployments remain available through an explicitly named legacy adapter while they migrate.
|
|
31
|
+
|
|
32
|
+
### SQLite is local storage
|
|
33
|
+
|
|
34
|
+
SQLite WAL provides local-only operation, a durable outbox, an inbox cache, cursor state, and a cheap pending-work check. Outbox writes use `synchronous=FULL`. It is not the canonical multi-machine database.
|
|
35
|
+
|
|
36
|
+
The gateway outbox queues immutable message publication. Receipt and lease mutations still require the remote authority because replaying them after ownership or identity changes can settle the wrong work.
|
|
37
|
+
|
|
38
|
+
Long-lived gateway MCP clients own a cancellable transport loop that replays publications and refreshes the inbox cache with bounded exponential backoff. It is transport maintenance, not agent monitoring. Manual MCP and CLI sync use the same replay path. A publication timeout after the gateway may have committed remains queued under its stable idempotency key. A retry resolves to the original immutable message instead of publishing a duplicate.
|
|
39
|
+
|
|
40
|
+
Local initialization must be idempotent. Every connection uses a bounded busy timeout. The minimum supported SQLite build must include the WAL-reset corruption fix. Node 22.23.1 on the current development machine embeds SQLite 3.51.3, which includes that fix.
|
|
41
|
+
|
|
42
|
+
### Portable archives cross storage engines without copying runtime state
|
|
43
|
+
|
|
44
|
+
A portable archive is a canonical NDJSON snapshot of one workspace's immutable
|
|
45
|
+
messages and read receipts. Export takes a consistent store snapshot. Verification
|
|
46
|
+
checks strict framing, canonical JSON, record order, counts, and the SHA-256 digest.
|
|
47
|
+
Import opens one private, regular archive file without following links. It performs a
|
|
48
|
+
format pass, a message pass, and a receipt pass through the same descriptor. Each pass
|
|
49
|
+
recomputes framing, order, counts, and the client-computed digest. Message and receipt
|
|
50
|
+
batches have row limits and a 4 MiB byte budget. Import runs as a rollback-backed dry
|
|
51
|
+
run unless the operator passes `--apply`; `--dry-run` and `--apply` are mutually
|
|
52
|
+
exclusive. Replaying identical records is idempotent. Conflicting message or receipt
|
|
53
|
+
content fails the import.
|
|
54
|
+
|
|
55
|
+
The portable format excludes deliveries, delivery events, presence, credentials,
|
|
56
|
+
owner-control state, security events, migration records, and edge synchronization
|
|
57
|
+
state. Those records have different authority and recovery semantics. The digest
|
|
58
|
+
detects changed bytes. It provides neither encryption nor authentication, so operators
|
|
59
|
+
must protect the archive as message data and use a separate trusted transfer channel.
|
|
60
|
+
|
|
61
|
+
Archive administration stays outside MCP, HTTPS, and the canonical operation registry.
|
|
62
|
+
Local commands open only a canonical local Agent Bridge database. PostgreSQL commands
|
|
63
|
+
use a separate restricted archive login supplied only through
|
|
64
|
+
`AGENT_BRIDGE_ARCHIVE_DATABASE_URL`. Export holds one store snapshot while it streams
|
|
65
|
+
canonical records and an incremental digest to a private temporary file. It fsyncs the
|
|
66
|
+
file, publishes without replacement unless `--force` is explicit, verifies the private
|
|
67
|
+
path, and fsyncs the directory. Only then does it complete the database audit. A
|
|
68
|
+
publication failure abandons the operation with a bounded code. The footer digest is
|
|
69
|
+
client-verified and caller-attested. It is not independent server proof.
|
|
70
|
+
|
|
71
|
+
Every export has a caller-visible request ID. The canonical header stores it as
|
|
72
|
+
`exportRequestId`, and the digest binds that header to the archive. Verification and
|
|
73
|
+
import return the same provenance. The request ID for an import operation remains a
|
|
74
|
+
separate destination-side identifier. An exact completed export retry verifies the
|
|
75
|
+
private file against the terminal export request ID, workspace, digest, and counts. A
|
|
76
|
+
retry of a started export never streams a new snapshot or force-replaces the file. It
|
|
77
|
+
verifies the retained file and uses the separate reconciliation operation, or leaves
|
|
78
|
+
the request started when the file is missing or mismatched. Temporary and backup paths
|
|
79
|
+
are deterministic same-directory names derived from `exportRequestId`. Force
|
|
80
|
+
replacement first makes a durable private hard-link backup. A pre-durable failure
|
|
81
|
+
restores and fsyncs the original before abandonment. An unproved restore, retained
|
|
82
|
+
artifact, or lost audit response returns its path and state without claiming a clean
|
|
83
|
+
failure. Reconciliation proves durability through the file descriptor that performed
|
|
84
|
+
verification and checks pathname identity around file and directory synchronization.
|
|
85
|
+
|
|
86
|
+
Portable v1 carries only records that satisfy the current message domain. New API
|
|
87
|
+
writes use lowercase UUIDs. Archive UUIDs must be lowercase, and archive timestamps
|
|
88
|
+
use UTC with six fractional digits. The archive path does not trim, deduplicate,
|
|
89
|
+
default, or rewrite message content. A legacy or direct database row outside this
|
|
90
|
+
domain fails export and requires native database recovery.
|
|
91
|
+
|
|
92
|
+
### Native DR preserves one storage authority
|
|
93
|
+
|
|
94
|
+
Native disaster recovery is separate from portable workspace archives. A native DR
|
|
95
|
+
bundle preserves one full local SQLite authority or the shared PostgreSQL authority in
|
|
96
|
+
its native schema. It does not translate between providers. Gateway edge SQLite files
|
|
97
|
+
are excluded because they contain a replayable outbox and cache, not the shared source
|
|
98
|
+
of truth.
|
|
99
|
+
|
|
100
|
+
The common framed format has a canonical manifest, provider-specific entries, and
|
|
101
|
+
per-entry SHA-256 hashes. Commands calculate and return a separate whole-bundle hash.
|
|
102
|
+
Readers validate strict framing, size bounds, entry order, provider kind, and exact
|
|
103
|
+
schema metadata. The hashes detect changed bytes but do not authenticate or encrypt
|
|
104
|
+
the backup. Paths must pass the private-path
|
|
105
|
+
policy. Publication uses an exclusive adjacent file, file synchronization, a no-replace
|
|
106
|
+
hard link, post-publication verification, and directory synchronization where the
|
|
107
|
+
platform supports it. Deterministic stage names include the operation UUID. Existing
|
|
108
|
+
stages fail closed and are returned as recovery paths. Node cannot prove parent-directory
|
|
109
|
+
entry synchronization on Windows. Successful Windows results report that directory
|
|
110
|
+
durability as unavailable while still requiring file-content synchronization and
|
|
111
|
+
post-publication verification.
|
|
112
|
+
|
|
113
|
+
Local backup accepts only a current or upgradeable local-authority SQLite database. It
|
|
114
|
+
uses the online backup API in a worker with a hard deadline, then verifies health and
|
|
115
|
+
the exact schema contract before framing the snapshot. Restore extracts through one
|
|
116
|
+
exclusive descriptor, verifies the contract, and publishes only to a new target. Edge
|
|
117
|
+
cache and legacy edge files are rejected. Worker termination that cannot be proved is
|
|
118
|
+
reported with the possible residue instead of being treated as a clean timeout.
|
|
119
|
+
|
|
120
|
+
PostgreSQL backup takes a repeatable-read exported snapshot while holding the native DR
|
|
121
|
+
advisory fence. `pg_dump` captures only the `agent_bridge` schema. A separate canonical
|
|
122
|
+
role inventory records the bounded role shells, membership options, and global or
|
|
123
|
+
schema-scoped default ACLs that a schema-only dump omits. The dump excludes data for
|
|
124
|
+
agent instances, rate-limit buckets, request authorities, and archive transaction
|
|
125
|
+
authorizations. The manifest binds migration inventory, table counts, claimed-delivery
|
|
126
|
+
count, tool major, role inventory, and the security, row-isolation, owner-control, and
|
|
127
|
+
portable-archive readiness definitions.
|
|
128
|
+
|
|
129
|
+
PostgreSQL restore requires a dedicated fresh database, the same database name and
|
|
130
|
+
server major, an exact-major `pg_restore`, and superuser authority. It restores role
|
|
131
|
+
shells as `NOLOGIN`, then schema objects, memberships, and default privileges. Claimed
|
|
132
|
+
deliveries become retrying deliveries with cleared leases. The restore checks migrations,
|
|
133
|
+
counts, role inventory, and every readiness attestation before success. External
|
|
134
|
+
principals remain `NOLOGIN`. A failure after target mutation begins disables new target
|
|
135
|
+
connections. If that offlining step fails, the command reports the target as unsafe and
|
|
136
|
+
lists residual role shells.
|
|
137
|
+
|
|
138
|
+
PostgreSQL source and target URLs come only from
|
|
139
|
+
`AGENT_BRIDGE_DR_SOURCE_DATABASE_URL` and
|
|
140
|
+
`AGENT_BRIDGE_DR_TARGET_DATABASE_URL`. Restore requires explicit acceptance that a
|
|
141
|
+
trusted PostgreSQL dump contains executable SQL. Source and restored target must never
|
|
142
|
+
run concurrently as authorities. Native DR supports PostgreSQL 15 through 18
|
|
143
|
+
and rejects a tool or server major mismatch.
|
|
144
|
+
|
|
145
|
+
### Messages are immutable
|
|
146
|
+
|
|
147
|
+
The protocol stores message content and routing fields in an immutable message record. Read and execution state live in separate tables.
|
|
148
|
+
|
|
149
|
+
Each message has:
|
|
150
|
+
|
|
151
|
+
- A client-generated UUIDv7 ID.
|
|
152
|
+
- A backend-assigned sequence number for the authoritative shared cursor.
|
|
153
|
+
- A workspace and source principal.
|
|
154
|
+
- A display source for v1 compatibility.
|
|
155
|
+
- Category, kind, and priority.
|
|
156
|
+
- Target principals or broadcast scope.
|
|
157
|
+
- Thread, reply, correlation, and causation IDs.
|
|
158
|
+
- Content and optional structured or referenced payload data.
|
|
159
|
+
- Creation and expiry timestamps.
|
|
160
|
+
- An optional idempotency key.
|
|
161
|
+
- An optional validated project label that is immutable with the message. It does not affect workspace, identity, delivery, or cursor authority.
|
|
162
|
+
- Optional atrib receipt and informed-by references.
|
|
163
|
+
|
|
164
|
+
Frequently queried fields are columns with indexes. Extension data remains JSON.
|
|
165
|
+
|
|
166
|
+
The v2 envelope uses fields that can be mapped to CloudEvents without changing stored message identity or payload data. Agent Bridge fields such as workspace, targets, thread, priority, expiry, and causal references remain protocol-specific extensions. A concrete CloudEvents serializer is deferred until an external integration needs one.
|
|
167
|
+
|
|
168
|
+
### Receipts and deliveries are different records
|
|
169
|
+
|
|
170
|
+
A receipt records that a principal read a message.
|
|
171
|
+
|
|
172
|
+
A publisher stores an immutable `deliveryPolicy` with each message. Untargeted messages default to mailbox mode. Targeted messages default to leased mode with `maxAttempts: 5`, `retryBaseDelayMs: 1000`, `retryMaxDelayMs: 60000`, and `retryJitterRatio: 0.2`. Leased policy may also set `notBefore`. Mailbox messages create no deliveries, and legacy mode rejects leased policy.
|
|
173
|
+
|
|
174
|
+
A leased delivery records executable work for a recipient. Its state is one of `pending`, `claimed`, `acked`, `retrying`, `dead`, or `cancelled`. `attempt` is lifetime-monotonic, while `cycleAttempt` resets on publisher requeue and `requeueCount` increments. Stored policy owns retry and exhaustion. Claims order due work by urgent, high, then info priority, followed by availability, message creation time, and the stable delivery ID tie-break.
|
|
175
|
+
|
|
176
|
+
Consumer-side `maxAttempts` on claim and `retryPolicy` on nack are compatibility inputs for one release. The service validates them and ignores them. This keeps older clients working without letting a consumer change immutable publisher intent.
|
|
177
|
+
|
|
178
|
+
Migration 010 uses `nack_retry` as a conservative compatibility mapping for a legacy `claimed` to `retrying` event. In v0.2 the same stored transition could come from an explicit nack or lease-expiry recovery, so the migrated action describes the retry transition and does not prove which cause produced it. When a legacy settlement event has no lease owner, migration recovers its actor from the preceding claim for the same delivery attempt.
|
|
179
|
+
|
|
180
|
+
A receipt does not change delivery state. A claim or settlement does not create a receipt. Lease renewal proves only that the current owner retained its claim. External task completion belongs to A2A or the application layer and must be recorded separately when a workflow needs both task state and delivery settlement.
|
|
181
|
+
|
|
182
|
+
Each delivery transition also appends an audit event. Runtime presence uses a separate leased record keyed by workspace, agent, and instance. Presence can carry a runtime type and declared capabilities without using a PID as ownership proof. Expired rows are pruned during normal presence operations. Each agent is limited to 128 active instances, and each workspace is limited to 4,096.
|
|
183
|
+
|
|
184
|
+
The system promises at-least-once delivery. It does not promise exactly-once execution. Idempotency keys prevent duplicate insertion, and consumers must make side effects idempotent.
|
|
185
|
+
|
|
186
|
+
Project participates in the idempotency fingerprint. Reads without a project filter span labeled and unlabeled messages inside the credential-bound workspace. An exact filter narrows that same cursor authority. Migration 008 adds the PostgreSQL column and index. Existing local and edge SQLite databases add their project columns during initialization. Migration 006 is not rewritten. A schema-owner command dry-runs or reconciles its rows into workspace `agent-bridge` in one transaction. It preserves IDs, timestamps, receipts, and row counts, and it verifies that no delivery exists before or after the change.
|
|
187
|
+
|
|
188
|
+
The legacy Supabase schema has no tenant workspace column. Its adapter reports workspace `*` and uses the legacy `project` column only as a message label. It rejects per-command workspace overrides because assigning a caller-selected workspace to global rows would create a false isolation boundary.
|
|
189
|
+
|
|
190
|
+
The delivery API supports:
|
|
191
|
+
|
|
192
|
+
- Atomic claim with row locking.
|
|
193
|
+
- Publisher-only cancel and dead/cancelled requeue, both fencing prior leases. Publisher and recipient can page deliveries and audit events; unrelated callers learn no existence information.
|
|
194
|
+
- Lease renewal for long-running work.
|
|
195
|
+
- Acknowledgment after successful processing.
|
|
196
|
+
- Negative acknowledgment with a bounded error message.
|
|
197
|
+
- Retry scheduling with exponential backoff and jitter.
|
|
198
|
+
- Maximum attempts and dead-letter state.
|
|
199
|
+
- Recovery of expired leases.
|
|
200
|
+
|
|
201
|
+
PostgreSQL claims use `FOR UPDATE SKIP LOCKED` so workers do not wait on rows that another worker already owns. SQLite uses a short immediate transaction to provide the same store contract on one machine.
|
|
202
|
+
|
|
203
|
+
### Cursor pull is authoritative
|
|
204
|
+
|
|
205
|
+
Reads use an opaque cursor backed by the backend sequence number. UUIDv7 improves index locality and offline identity, but it does not replace the server cursor because client clocks can drift. Filters run in the storage layer. Clients never fetch an arbitrary recent sample and filter it in memory.
|
|
206
|
+
|
|
207
|
+
During gateway outages, normal inbox and pending reads return the locally cached candidate set with explicit degraded and stale metadata. Because receipt state is remote authority and is not mirrored, an offline `unacknowledgedBy` result reports acknowledgement state as unknown; it must not be interpreted as proof that every returned message is unread.
|
|
208
|
+
|
|
209
|
+
Realtime, hooks, webhooks, and desktop notifications may wake a client. The client still pulls from its last durable cursor. This closes notification gaps and makes reconnect behavior testable.
|
|
210
|
+
|
|
211
|
+
Supabase documents that Realtime does not guarantee every database change. Agent Bridge therefore treats it as an optional delivery hint, not a message log.
|
|
212
|
+
|
|
213
|
+
### Identity is bound at the client boundary
|
|
214
|
+
|
|
215
|
+
The identity model separates:
|
|
216
|
+
|
|
217
|
+
- Workspace.
|
|
218
|
+
- Agent principal.
|
|
219
|
+
- Human-readable name.
|
|
220
|
+
- Runtime type.
|
|
221
|
+
- Client instance.
|
|
222
|
+
- Session or thread.
|
|
223
|
+
- Optional role.
|
|
224
|
+
- Authentication subject and credential.
|
|
225
|
+
|
|
226
|
+
Remote credentials bind the workspace and principal. Each installed client gets a separate owner-only backend file containing only its bound token and backend settings. The shared config does not hold a gateway token. A client cannot claim another source label. Local mode binds identity through its process configuration. `AGENT_BRIDGE_AGENT` remains the compatibility field for MCP and CLI clients.
|
|
227
|
+
|
|
228
|
+
### The core is provider-neutral
|
|
229
|
+
|
|
230
|
+
Protocol logic depends on a `BridgeStore` interface, not a provider SDK. The initial stores are:
|
|
231
|
+
|
|
232
|
+
- SQLite for local mode and edge state.
|
|
233
|
+
- PostgreSQL for a standalone shared deployment.
|
|
234
|
+
- Supabase legacy REST for v1 compatibility during migration.
|
|
235
|
+
- Agent Bridge HTTP API for normal remote clients.
|
|
236
|
+
|
|
237
|
+
Runtime and wakeup adapters are independent from storage. A client manifest declares identity injection, supported wake modes, config locations, health checks, and install actions for Codex, Claude Code, Claude Desktop, OpenClaw, or a generic MCP client.
|
|
238
|
+
|
|
239
|
+
### v1 compatibility is additive
|
|
240
|
+
|
|
241
|
+
The MCP tools `post_context`, `get_context`, and `ack_context` remain available. Existing CLI verbs and flags remain accepted. Text responses remain present while v2 adds structured result data.
|
|
242
|
+
|
|
243
|
+
Legacy Supabase rows remain readable as broadcast messages. A migration command checks schema state, backfills v2 records, and verifies counts before activation. Secure shared mode cannot silently inherit the old permissive RLS policies.
|
|
244
|
+
|
|
245
|
+
## Security boundary
|
|
246
|
+
|
|
247
|
+
Shared mode uses a token-authenticated API. The service stores only token hashes. Credentials bind a workspace and principal, carry canonical operation scopes, can expire, and can be revoked. Capabilities requires an active credential but no named scope. Local and legacy providers do not claim gateway scope enforcement.
|
|
248
|
+
|
|
249
|
+
Migration 011 grants every pre-migration credential the full compatibility scope set, including expired and revoked rows, without rewriting lifecycle metadata. Migration 014 preserves those rows but changes new raw inserts to empty scopes. Owner provisioning selects an immutable named scope set. Database constraints reject unknown, duplicated, or unsorted scopes.
|
|
250
|
+
|
|
251
|
+
A successor holds an immutable link to one predecessor in the same workspace and principal. Revocation and ordinary expiry always win. A grace cutoff applies only after a successor exists and can only shorten the predecessor's remaining lifetime. Narrow owner functions commit replacement or revocation with their security event. Expected replacement failures that identify a predecessor also produce a fixed failure event.
|
|
252
|
+
|
|
253
|
+
Schema migration and provisioning use a database-owner connection. The running gateway uses a different login that inherits a database-specific runtime role. The role name includes a digest of the database name so permission does not bleed between Agent Bridge databases on one PostgreSQL cluster. That role can read runtime state, insert immutable messages and receipts, transition deliveries, append delivery events, and maintain leased presence. It cannot change workspaces, agents, credentials, message content, schema objects, or migration records. Gateway startup does not run migrations.
|
|
254
|
+
|
|
255
|
+
Migration 014 adds a separate offline owner control plane. Database-specific no-login control-owner, operator, and auditor roles have no superuser or RLS bypass powers. The control owner owns append-only request, event, membership, and catalog-attestation records. It can select only the non-secret credential columns used by its security-definer functions. Operators can provision several principals in a name-compatible workspace, rotate credentials, revoke credentials, and read safe inventory. Auditors can only read inventory.
|
|
256
|
+
|
|
257
|
+
The owner CLI connects only through `AGENT_BRIDGE_OPERATOR_DATABASE_URL` and remains
|
|
258
|
+
outside MCP, HTTP, and the versioned operation registry. Provision and rotation write
|
|
259
|
+
a revisioned enrollment file with exact gateway, workspace, principal, runtime, and
|
|
260
|
+
instance inputs before any database call. The file moves through `pending`, `ready`,
|
|
261
|
+
`consuming`, and `consumed` states under one exclusive per-file operation lock. Every
|
|
262
|
+
transition compares the current disk revision, state, request, operation, and token
|
|
263
|
+
before durable atomic replacement. Stale lock recovery is explicit and requires
|
|
264
|
+
same-host ownership, a minimum age, and proof that the recorded process has stopped.
|
|
265
|
+
Only the local process computes the token hash. PostgreSQL never receives the raw
|
|
266
|
+
token. The installer registers a provisioned client once. Provision stores nonsecret
|
|
267
|
+
credential, principal, and instance metadata in the client backend. Rotation requires
|
|
268
|
+
that metadata plus an exact live host registration before it replaces the token. An
|
|
269
|
+
opaque inventory cursor includes its workspace authority so it cannot be reused under
|
|
270
|
+
another filter.
|
|
271
|
+
|
|
272
|
+
Enrollment deletion and lock release report durability separately. A failure before
|
|
273
|
+
unlink leaves a retained consumed file. A failure after unlink reports unknown directory
|
|
274
|
+
durability and never reports a retained path. On Windows, enrollment roots, every path
|
|
275
|
+
component, enrollment files, temporary files, locks, and credential backends must be
|
|
276
|
+
owned by the current account SID or the active token's default owner SID before the
|
|
277
|
+
process applies policy. The process rejects every other owner, sets the account SID as
|
|
278
|
+
owner, and verifies one protected account-SID FullControl rule. Verification-only paths
|
|
279
|
+
must already satisfy that final policy. Node identity and file-type checks run before
|
|
280
|
+
and after the native policy check. Native reparse attributes
|
|
281
|
+
reject symlinks, junctions, and other reparse objects before and after DACL work.
|
|
282
|
+
|
|
283
|
+
The schema owner is the trusted offline role administrator. It holds all three control roles with the admin option and registers eligible login roles through `register_control_member`; `revoke_control_member` removes them. Those functions are not available through HTTP or MCP. Each call is idempotent by request UUID and appends the database session actor to the membership ledger. Runtime readiness compares the active registry with the full `pg_has_role` closure and the direct `pg_auth_members` edges. It rejects unregistered operator or auditor holders, every external owner holder, missing registered grants, unsafe login attributes, extra roles inherited by a registered member, and roles that inherit a registered member. Direct edges must retain their expected grantor, admin, inherit, and set options. Only the schema owner is exempt from the broad closure check because a PostgreSQL superuser reports membership across roles. A direct `GRANT` is never a valid deployment shortcut.
|
|
284
|
+
|
|
285
|
+
Every protected operation first locks a member-global key for `session_user`, followed by capability keys in operator-before-auditor order. It then checks the active registry, direct membership edge, safe login attributes, and complete upstream and downstream role closure. Inventory accepts either registered capability. Registration and revocation use the same global-first lock hierarchy. Opposite operator and auditor changes for one member therefore cannot deadlock. A revocation waits for an operation that already passed authorization, while every operation begun after revocation observes the new state. A session that remains in a revoked role through `SET ROLE` cannot use that stale authorization.
|
|
286
|
+
|
|
287
|
+
Every mutation then locks its request UUID before reading the request ledger or lifecycle rows. The ledger stores a SHA-256 fingerprint of the canonical request. Timestamp inputs use epoch microseconds in that fingerprint, so session time zones cannot change replay identity. Identical concurrent calls return the first committed result, including a provisioning replay after its requested expiry. Changed content under the same UUID fails. Rotation accepts credentials without an expiry. A null grace cutoff invalidates the predecessor as soon as the successor exists. Audit actors come from `session_user`; results, audit rows, and sanitized constraint errors exclude credential digest material. Text fields at the control boundary reject surrounding whitespace, control characters, and values over 128 characters.
|
|
288
|
+
|
|
289
|
+
Rotation includes the expected workspace and principal in its request fingerprint. The
|
|
290
|
+
function checks both against the predecessor while holding its row lock. Its initial
|
|
291
|
+
and replay results return the canonical workspace and principal from the stored
|
|
292
|
+
request result. The owner CLI uses that result directly instead of performing a
|
|
293
|
+
bounded inventory lookup after rotation.
|
|
294
|
+
|
|
295
|
+
Inventory orders credentials by millisecond-normalized creation time and credential ID. Callers may filter by workspace, continue after a `(created_at, credential_id)` cursor, and request up to 1,000 rows. The default page contains 100 rows. Fixed-origin `date_bin` expression indexes cover both global and workspace ordering. The millisecond normalization matches the timestamp precision preserved by common PostgreSQL JavaScript clients.
|
|
296
|
+
|
|
297
|
+
Before recording an attestation, the migration restores the canonical row-isolation helper functions and verifies the existing row-isolation attestation. An independent credential-security catalog checks exact relation, column, constraint, index, trigger, and function definitions against a separately certified digest for PostgreSQL 15, 16, 17, and 18. An unknown future major has no expected digest and fails closed. Runtime readiness repeats the explicit major-version gate on every call. An already-migrated database therefore becomes unavailable after an upgrade to an uncertified PostgreSQL major. The migration also checks relation ownership, RLS state, protected ACLs, default privileges, and control-role ownership. It refuses to establish a healthy baseline over dependency drift, privilege paths to untrusted roles, or objects unexpectedly owned by a control role.
|
|
298
|
+
|
|
299
|
+
The stored definition is scoped to the control plane and its dependencies. It includes the live credential-security prerequisite catalog, protected function bodies and owners, trigger and index definitions, column types and defaults, RLS and persistence flags, role attributes, relevant default privileges, and the allowed schema, relation, column, sequence, and function grants. Runtime readiness compares the live definition with that record and checks registered membership separately. It fails when a protected function body or same-name constraint changes, a ledger trigger is disabled, a credential default drifts, a control role inherits a data role, or an untrusted role receives schema access, data access, function execution, or a future-object default grant. An unrelated owner-only table remains outside the definition and does not cause a false readiness failure.
|
|
300
|
+
|
|
301
|
+
Migration 012 removes direct runtime reads of credential digest material and establishes transaction-bound request authority. Node computes the credential hash, so PostgreSQL never receives the raw bearer credential. Request authority, security accounting, and domain work use one checked-out PostgreSQL client and one explicit transaction. A security-definer opener matches the credential ID and hash against current revocation, expiry, successor-grace, agent, and workspace state in one locked statement, then records the database-derived credential, workspace, principal, and scopes for that backend and transaction. Instance remains validated same-principal attribution and never establishes authority. Claim, cancel, and requeue reuse the outer transaction without nested `BEGIN`.
|
|
302
|
+
|
|
303
|
+
The domain savepoint separates expected rejected domain work from security effects: expected scope and rate accounting can commit while failed domain mutations are rolled back. An abort before commit rolls the whole request back. Once commit dispatch begins, the gateway never retries and reports `mutation_outcome_unknown` if the outcome is ambiguous. A failed rollback discards the pooled connection.
|
|
304
|
+
|
|
305
|
+
Migration 013 enables and forces RLS on the five domain tables: messages, receipts, deliveries, delivery events, and agent instances. A database-specific no-login role owns those tables. Two more no-login roles isolate request-context reads and delivery-event writes. The gateway login inherits only the runtime role, which has `NOBYPASSRLS` and cannot inherit any of the owner roles. Zero-argument stable context functions read the workspace and principal from the authority row bound to the current backend, transaction ID, and session user. Policies call those functions through scalar subqueries so PostgreSQL evaluates them as InitPlans rather than once per row.
|
|
306
|
+
|
|
307
|
+
Message policies allow the source, broadcast readers, and named targets. Receipt rows belong to one principal. Delivery and event visibility is limited to the publisher or recipient. Presence is visible across one workspace, while writes belong to the active principal. A delivery actor trigger blocks publishers from claim or settlement actions and blocks recipients from cancel or requeue. It does not validate every lifecycle transition or lease token. Those checks remain in the service. Target-to-delivery membership also remains application-enforced; the database proves the delivery's message and publisher, not that its recipient appears in the target array.
|
|
308
|
+
|
|
309
|
+
The migration stores the server's own deparsed catalog definition after creating the protected objects. Runtime readiness compares current policies, constraints, trigger bindings, and security functions with that baseline. This avoids a PostgreSQL-version-specific source hash. The runtime can read the attestation but cannot change it. Owner-approved schema changes must replace the baseline in the same migration. The gateway coalesces readiness probes, caches their result for one second, and runs them through a separate one-connection pool. A capabilities request therefore never waits for a pool slot that its request-authority transaction already holds. Capabilities report row isolation only when request authority exists and every readiness check passes. Superusers and `BYPASSRLS` roles bypass PostgreSQL RLS by design and are forbidden for the gateway login.
|
|
310
|
+
|
|
311
|
+
The database denies direct message updates. Constrained operations handle insertion, receipts, claims, renewals, acknowledgments, and negative acknowledgments. Every query includes workspace scope, and recipient visibility is enforced in storage rather than by client-side filtering.
|
|
312
|
+
|
|
313
|
+
Limits apply to content bytes, payload bytes, metadata depth, target count, batch size, lease duration, and page size. Network calls have connect and total deadlines. Every authenticated operation consumes a database-timed credential-wide bucket and a separate operation bucket. Numeric bucket state, deterministic row locking, and atomic denial events keep concurrent decisions consistent. Missing or disabled policy state fails closed. Errors have stable codes and do not return secrets or raw database messages.
|
|
314
|
+
|
|
315
|
+
## Operations and observability
|
|
316
|
+
|
|
317
|
+
The CLI provides `init`, `doctor`, `status`, `demo`, `send`, `inbox`, `history`, `claim`, `ack`, `nack`, `watch`, `sync`, portable archive, and migration commands. Existing `post` and `get` aliases remain available.
|
|
318
|
+
|
|
319
|
+
`/readyz` reports storage and schema readiness. Authenticated HTTP status reports the bound principal, provider schema, delivery counts, and the oldest due delivery. CLI `doctor` and `status` use a separate client-status contract that also reports local edge state and remote gateway reachability.
|
|
320
|
+
|
|
321
|
+
Gateway responses carry a request ID, including stable error envelopes. Scope failures return required scopes under `error.details`. Rate failures return a rounded retry delay in both the HTTP header and error details. Authenticated Prometheus output counts requests, errors, timeouts, and authentication failures. Responses, metrics, and append-only security events exclude bearer tokens, hashes, database URLs, payload bodies, arbitrary metadata, and credential material.
|
|
322
|
+
|
|
323
|
+
## Packaging and release
|
|
324
|
+
|
|
325
|
+
The unscoped npm name `agent-bridge` belongs to another project. This repository uses `@creatornader/agent-bridge`. Tagged builds always produce a package artifact. Publishing stays gated by the protected `npm` environment, npm's OIDC trusted-publisher binding to `release.yml`, and the `NPM_PUBLISH_ENABLED` repository variable.
|
|
326
|
+
|
|
327
|
+
The package must contain built runtime files, migrations, client manifests, maintained
|
|
328
|
+
public documentation, the license, README, roadmap, security policy, and changelog. It
|
|
329
|
+
exports the portable archive API from `@creatornader/agent-bridge/archive`. A clean
|
|
330
|
+
tarball install runs archive and CLI smoke checks in CI. Releases use one version
|
|
331
|
+
source, a tag-to-version check, and npm provenance. The npm environment does not
|
|
332
|
+
currently require a human reviewer, so the release process must not claim a manual
|
|
333
|
+
approval gate.
|
|
334
|
+
|
|
335
|
+
## Acceptance checks
|
|
336
|
+
|
|
337
|
+
The TypeBox 1.x registry validates closed requests before domain semantics. Response schemas require known fields and accept additive properties. MCP and HTTP 2.1 claim and delivery-control results use object envelopes with nullable `delivery` fields. HTTP 2.0 and the unversioned CLI keep their released direct or null shapes.
|
|
338
|
+
|
|
339
|
+
Compatibility is asymmetric. An upgraded gateway serves released headerless and explicit 2.0 clients. A new 2.1 client probes before mutation and accepts the gateway only when complete, consistent response headers select 2.1 and advertise 2.1 support. A headerless response, selected 2.0 response, or partial negotiation means the gateway must be upgraded. The client rejects mutation instead of using the d8184fe 2.0 contract. Deploy the gateway before 2.1 clients.
|
|
340
|
+
|
|
341
|
+
The OpenAPI paths describe protocol 2.1. Embedded 2.0 vendor extensions carry only the frozen compatibility schemas and metadata needed to document released clients; they do not form a second OpenAPI description. `npm run contracts:check` proves that JSON Schema 2020-12, OpenAPI 3.1.2, MCP manifest, and capability artifacts match the registry. All schema references are local. Protocol, npm package, MCP implementation, and migration versions are independent. Gateway scope enforcement comes from the registry. Provider-neutral artifacts report enforcement separately for gateway, local, and legacy modes.
|
|
342
|
+
|
|
343
|
+
The v2 implementation is not accepted until all of these checks pass:
|
|
344
|
+
|
|
345
|
+
1. Unit tests and the production build.
|
|
346
|
+
2. One store contract suite against SQLite and PostgreSQL.
|
|
347
|
+
3. Concurrent claim tests that prove one active lease per delivery.
|
|
348
|
+
4. Crash-after-claim recovery through lease expiry.
|
|
349
|
+
5. Idempotent insertion and retry tests.
|
|
350
|
+
6. Cursor replay with equal timestamps and expiry filtering.
|
|
351
|
+
7. Workspace and recipient isolation tests.
|
|
352
|
+
8. HTTP timeout, authentication, redaction, and malformed-input tests.
|
|
353
|
+
9. MCP transport tests for the three v1 tools and all added v2 tools.
|
|
354
|
+
10. CLI config precedence, identity, local demo, doctor, and package-install tests.
|
|
355
|
+
11. Upgrade tests from the current `shared_context` schema.
|
|
356
|
+
12. A two-client local run and a two-client PostgreSQL run.
|
|
357
|
+
|
|
358
|
+
## Deferred work
|
|
359
|
+
|
|
360
|
+
NATS, JetStream, SLIM, or another transport stays optional until measured throughput, wakeup, queue-group, or geographic requirements justify it. Any transport adapter must preserve cursored replay, idempotency, identity binding, and delivery leases. A dashboard also waits. The CLI and structured health surface come first.
|
|
361
|
+
|
|
362
|
+
End-to-end payload encryption needs a separate threat model and key-management design. The current ciphertext field does not imply that routing metadata is encrypted. OpenClaw and generic MCP config mutation remains operator-managed because their host config formats are not stable enough for blind edits.
|
|
363
|
+
|
|
364
|
+
## Source notes
|
|
365
|
+
|
|
366
|
+
- agmsg current architecture and implementation: <https://github.com/fujibee/agmsg>
|
|
367
|
+
- SQLite WAL behavior: <https://sqlite.org/wal.html>
|
|
368
|
+
- SQLite WAL-reset bug: <https://sqlite.org/forum/forumpost/0b7fd7f028>
|
|
369
|
+
- PostgreSQL row locking and `SKIP LOCKED`: <https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE>
|
|
370
|
+
- Supabase self-hosting responsibilities: <https://supabase.com/docs/guides/self-hosting>
|
|
371
|
+
- Supabase Realtime delivery limitations: <https://supabase.com/docs/reference/self-hosting-realtime>
|
|
372
|
+
- MCP tools specification: <https://modelcontextprotocol.io/specification/2025-11-25/server/tools>
|
|
373
|
+
- MCP transport specification: <https://modelcontextprotocol.io/specification/2025-11-25/basic/transports>
|
|
374
|
+
- UUIDv7: <https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7>
|
|
375
|
+
- CloudEvents core attributes: <https://github.com/cloudevents/spec/blob/ce%40stable/cloudevents/spec.md#required-attributes>
|