@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +467 -29
  3. package/ROADMAP.md +104 -0
  4. package/SECURITY.md +73 -0
  5. package/SKILL.md +68 -2
  6. package/bin/agent-bridge +2 -2
  7. package/clients/claude-desktop.json +3 -1
  8. package/dist/archive.d.ts +163 -0
  9. package/dist/archive.js +50 -0
  10. package/dist/archive.js.map +1 -0
  11. package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
  12. package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
  13. package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
  14. package/dist/bridge.d.ts +76 -17
  15. package/dist/bridge.js +52 -24
  16. package/dist/chunk-2DFNOLMO.js +618 -0
  17. package/dist/chunk-2DFNOLMO.js.map +1 -0
  18. package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
  19. package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
  20. package/dist/chunk-45BZGB77.js +709 -0
  21. package/dist/chunk-45BZGB77.js.map +1 -0
  22. package/dist/chunk-4VB44KJU.js +230 -0
  23. package/dist/chunk-4VB44KJU.js.map +1 -0
  24. package/dist/chunk-5GMB3A77.js +1013 -0
  25. package/dist/chunk-5GMB3A77.js.map +1 -0
  26. package/dist/chunk-A6W4QALF.js +1258 -0
  27. package/dist/chunk-A6W4QALF.js.map +1 -0
  28. package/dist/chunk-BZLMF5O5.js +328 -0
  29. package/dist/chunk-BZLMF5O5.js.map +1 -0
  30. package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
  31. package/dist/chunk-CEXBMOI5.js.map +1 -0
  32. package/dist/chunk-GADH244D.js +527 -0
  33. package/dist/chunk-GADH244D.js.map +1 -0
  34. package/dist/chunk-MBPZXTJA.js +2428 -0
  35. package/dist/chunk-MBPZXTJA.js.map +1 -0
  36. package/dist/chunk-NCO7RLBM.js +146 -0
  37. package/dist/chunk-NCO7RLBM.js.map +1 -0
  38. package/dist/chunk-Q6PPMXJC.js +1050 -0
  39. package/dist/chunk-Q6PPMXJC.js.map +1 -0
  40. package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
  41. package/dist/chunk-SPSMZOCJ.js.map +1 -0
  42. package/dist/chunk-UAHMP6GC.js +54 -0
  43. package/dist/chunk-UAHMP6GC.js.map +1 -0
  44. package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
  45. package/dist/chunk-VOQIDTCJ.js.map +1 -0
  46. package/dist/chunk-WAI2IYIQ.js +554 -0
  47. package/dist/chunk-WAI2IYIQ.js.map +1 -0
  48. package/dist/cli.d.ts +2 -1
  49. package/dist/cli.js +3577 -250
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client-config.d.ts +1 -1
  52. package/dist/client-config.js +1 -1
  53. package/dist/client-runtime.d.ts +10 -6
  54. package/dist/client-runtime.js +9 -6
  55. package/dist/contracts/generate.d.ts +2 -0
  56. package/dist/contracts/generate.js +247 -0
  57. package/dist/contracts/generate.js.map +1 -0
  58. package/dist/dr.d.ts +324 -0
  59. package/dist/dr.js +124 -0
  60. package/dist/dr.js.map +1 -0
  61. package/dist/gateway-CggQ8vTC.d.ts +78 -0
  62. package/dist/gateway-main.js +190 -8
  63. package/dist/gateway-main.js.map +1 -1
  64. package/dist/gateway.d.ts +4 -3
  65. package/dist/gateway.js +4 -3
  66. package/dist/index.js +11 -8
  67. package/dist/index.js.map +1 -1
  68. package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
  69. package/dist/server.d.ts +3 -2
  70. package/dist/server.js +13 -8
  71. package/dist/sqlite.d.ts +94 -12
  72. package/dist/sqlite.js +7 -4
  73. package/docs/architecture-v2.md +375 -0
  74. package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
  75. package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
  76. package/docs/ecosystem.md +54 -0
  77. package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
  78. package/docs/troubleshooting.md +89 -0
  79. package/llms.txt +96 -4
  80. package/openapi/agent-bridge-v2.openapi.json +9610 -0
  81. package/package.json +24 -4
  82. package/schemas/agent-bridge-v2.capabilities.json +1829 -0
  83. package/schemas/agent-bridge-v2.mcp.json +2074 -0
  84. package/schemas/agent-bridge-v2.schema.json +4560 -0
  85. package/sql/migrations/008_message_projects.sql +36 -0
  86. package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
  87. package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
  88. package/sql/migrations/011_credential_security.sql +875 -0
  89. package/sql/migrations/012_request_authority.sql +177 -0
  90. package/sql/migrations/013_row_isolation.sql +495 -0
  91. package/sql/migrations/014_owner_control_plane.sql +1641 -0
  92. package/sql/migrations/015_portable_archives.sql +1813 -0
  93. package/sql/migrations/016_native_dr_fence.sql +1005 -0
  94. package/bin/CLAUDE.md +0 -11
  95. package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
  96. package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
  97. package/dist/chunk-4KAOBC5X.js.map +0 -1
  98. package/dist/chunk-6MAZ6YLZ.js +0 -146
  99. package/dist/chunk-6MAZ6YLZ.js.map +0 -1
  100. package/dist/chunk-AXL2I7FH.js.map +0 -1
  101. package/dist/chunk-BV6W4AJK.js +0 -211
  102. package/dist/chunk-BV6W4AJK.js.map +0 -1
  103. package/dist/chunk-CKSKUBDQ.js +0 -153
  104. package/dist/chunk-CKSKUBDQ.js.map +0 -1
  105. package/dist/chunk-JOUBDRYX.js +0 -272
  106. package/dist/chunk-JOUBDRYX.js.map +0 -1
  107. package/dist/chunk-JTC5YURG.js +0 -49
  108. package/dist/chunk-JTC5YURG.js.map +0 -1
  109. package/dist/chunk-O26W5QHS.js +0 -409
  110. package/dist/chunk-O26W5QHS.js.map +0 -1
  111. package/dist/chunk-WEULPMUY.js +0 -988
  112. package/dist/chunk-WEULPMUY.js.map +0 -1
  113. package/dist/chunk-YD6YJATN.js.map +0 -1
  114. 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, D as DeliveryState, R as RetryPolicy, A as AgentPresence, a as BridgeDeliveryEvent } from './bridge-domain-BcnBeHOn.js';
2
- import { b as MessageQuery, M as MessagePage, B as BridgeDiagnostics, I as InsertMessageResult, a as BridgeStore, C as ClaimOptions } from './bridge-store-BuGjxTn4.js';
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, cacheVisible: boolean, now?: Date): Promise<void>;
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
- stats(): Promise<EdgeStats>;
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(workspace: string, messageIds: string[], principal: string, readAt?: Date): Promise<number>;
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: string | undefined, retryPolicy: RetryPolicy): Promise<BridgeDelivery | null>;
128
- diagnostics(principal: BridgePrincipal): Promise<SyncDiagnostics>;
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(workspace: string, messageIds: string[], principal: string, readAt?: Date): Promise<number>;
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: string | undefined, retryPolicy: RetryPolicy): Promise<BridgeDelivery | null>;
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
- listDeliveryEvents(deliveryId: string): Promise<BridgeDeliveryEvent[]>;
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-WEULPMUY.js";
12
- import "./chunk-YD6YJATN.js";
13
- import "./chunk-BV6W4AJK.js";
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>