@securecoms/agent-bridge 0.29.0 → 0.30.1
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/README.md +156 -9
- package/dist/bin.d.ts +83 -1
- package/dist/bin.js +3395 -586
- package/dist/bridge-agent.d.ts +269 -2
- package/dist/config.d.ts +7 -0
- package/dist/consumer-grants.d.ts +19 -0
- package/dist/errors.d.ts +1 -1
- package/dist/grants/binding.d.ts +58 -0
- package/dist/grants/effective.d.ts +47 -0
- package/dist/grants/manifest.d.ts +68 -0
- package/dist/grants/owner-turn.d.ts +52 -0
- package/dist/grants/persona-render.d.ts +30 -0
- package/dist/grants/pin-command.d.ts +31 -0
- package/dist/grants/pin.d.ts +35 -0
- package/dist/grants/room-search-cap.d.ts +15 -0
- package/dist/grants/voice-fence.d.ts +53 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/index.js +2289 -192
- package/dist/memory/unified/item-book.d.ts +3 -0
- package/dist/memory/unified/migrate.d.ts +11 -0
- package/dist/memory/unified/read-book.d.ts +2 -1
- package/dist/memory/unified/worker-protocol.d.ts +4 -0
- package/dist/memory-taps.d.ts +1 -1
- package/dist/memory-worker.js +18 -3
- package/dist/service/install-prep.d.ts +21 -1
- package/dist/service/instance-lock.d.ts +88 -0
- package/dist/service/launchd.d.ts +1 -1
- package/dist/service/runtime-config-dir.d.ts +15 -0
- package/dist/service/status.d.ts +3 -5
- package/dist/service/systemd.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +5 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13317,17 +13317,17 @@ async function makeNobleSignatureImpl(alg) {
|
|
|
13317
13317
|
};
|
|
13318
13318
|
}
|
|
13319
13319
|
try {
|
|
13320
|
-
const { ed25519:
|
|
13320
|
+
const { ed25519: ed255197 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
13321
13321
|
return {
|
|
13322
13322
|
async sign(signKey, message) {
|
|
13323
|
-
return
|
|
13323
|
+
return ed255197.sign(message, signKey);
|
|
13324
13324
|
},
|
|
13325
13325
|
async verify(publicKey, message, signature) {
|
|
13326
|
-
return
|
|
13326
|
+
return ed255197.verify(signature, message, publicKey);
|
|
13327
13327
|
},
|
|
13328
13328
|
async keygen() {
|
|
13329
|
-
const signKey =
|
|
13330
|
-
return { signKey, publicKey:
|
|
13329
|
+
const signKey = ed255197.utils.randomSecretKey();
|
|
13330
|
+
return { signKey, publicKey: ed255197.getPublicKey(signKey) };
|
|
13331
13331
|
}
|
|
13332
13332
|
};
|
|
13333
13333
|
} catch (err) {
|
|
@@ -17605,11 +17605,11 @@ var MlsOperationQueue = class _MlsOperationQueue {
|
|
|
17605
17605
|
const prev = this.chains.get(channelId) ?? Promise.resolve();
|
|
17606
17606
|
const entry = { group: this.group, kind: this.kind, key: channelId, released: false };
|
|
17607
17607
|
const parentHeld = als?.getStore() ?? [];
|
|
17608
|
-
const
|
|
17608
|
+
const guarded2 = als ? () => als.run([...parentHeld, entry], op) : op;
|
|
17609
17609
|
const settle = (p) => p.finally(() => {
|
|
17610
17610
|
entry.released = true;
|
|
17611
17611
|
});
|
|
17612
|
-
const next = settle(prev.then(
|
|
17612
|
+
const next = settle(prev.then(guarded2, guarded2));
|
|
17613
17613
|
this.chains.set(channelId, next);
|
|
17614
17614
|
this.inFlight.set(channelId, (this.inFlight.get(channelId) ?? 0) + 1);
|
|
17615
17615
|
try {
|
|
@@ -23342,7 +23342,9 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23342
23342
|
retryCount: 0,
|
|
23343
23343
|
expires_at: row.expires_at,
|
|
23344
23344
|
voiceMode: row.voice_mode ?? void 0,
|
|
23345
|
-
voiceDefaultResponder: row.voice_default_responder ?? void 0
|
|
23345
|
+
voiceDefaultResponder: row.voice_default_responder ?? void 0,
|
|
23346
|
+
...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
|
|
23347
|
+
payloadHex: row.payload
|
|
23346
23348
|
};
|
|
23347
23349
|
const custodiedAt = this.nowMs();
|
|
23348
23350
|
this.custody.set(row.queue_id, entry);
|
|
@@ -24089,6 +24091,7 @@ function base64UrlNoPad(s) {
|
|
|
24089
24091
|
}
|
|
24090
24092
|
var BEARER_SUBPROTOCOL = "securecoms.bearer.v1";
|
|
24091
24093
|
var WS_CLOSE_IDENTITY_REVOKED = 4006;
|
|
24094
|
+
var WS_CLOSE_SESSION_SUPERSEDED = 4409;
|
|
24092
24095
|
function isIdentityRevokedClose(code, reason) {
|
|
24093
24096
|
return code === WS_CLOSE_IDENTITY_REVOKED || reason === "identity_revoked";
|
|
24094
24097
|
}
|
|
@@ -24304,7 +24307,7 @@ function openDeliveryPings(o) {
|
|
|
24304
24307
|
end();
|
|
24305
24308
|
};
|
|
24306
24309
|
if (!hardened) {
|
|
24307
|
-
if (code === 4001 || isIdentityRevokedClose(code, reason)) {
|
|
24310
|
+
if (code === 4001 || code === WS_CLOSE_SESSION_SUPERSEDED || isIdentityRevokedClose(code, reason)) {
|
|
24308
24311
|
terminalClose();
|
|
24309
24312
|
return;
|
|
24310
24313
|
}
|
|
@@ -24313,7 +24316,7 @@ function openDeliveryPings(o) {
|
|
|
24313
24316
|
}
|
|
24314
24317
|
const tr = terminalReason;
|
|
24315
24318
|
terminalReason = void 0;
|
|
24316
|
-
if (code === 4006 || tr && code === 4001) {
|
|
24319
|
+
if (code === 4006 || code === WS_CLOSE_SESSION_SUPERSEDED || tr && code === 4001) {
|
|
24317
24320
|
lifecycle("terminal", tr ?? `code ${code}`);
|
|
24318
24321
|
terminalClose();
|
|
24319
24322
|
return;
|
|
@@ -24470,10 +24473,14 @@ var RestTransport = class {
|
|
|
24470
24473
|
f;
|
|
24471
24474
|
ws;
|
|
24472
24475
|
tokenProvider;
|
|
24473
|
-
/** The
|
|
24474
|
-
* cleared
|
|
24475
|
-
* a no-op when no subscription is active. */
|
|
24476
|
+
/** The shared delivery-ping socket's send, set while that socket is open and
|
|
24477
|
+
* cleared when it closes (last subscriber gone, or terminal). Control frames
|
|
24478
|
+
* (status keepalive) route through it; a no-op when no subscription is active. */
|
|
24476
24479
|
_controlSend = null;
|
|
24480
|
+
/** P-5: the one delivery-ping stream this transport holds, shared by every subscriber. */
|
|
24481
|
+
pingHub = null;
|
|
24482
|
+
/** Set once the shared stream ends on a terminal close; no socket is ever reopened. */
|
|
24483
|
+
pingsTerminal = false;
|
|
24477
24484
|
constructor(opts) {
|
|
24478
24485
|
this.opts = opts;
|
|
24479
24486
|
this.f = opts.fetchImpl ?? fetch;
|
|
@@ -24575,25 +24582,52 @@ var RestTransport = class {
|
|
|
24575
24582
|
idempotentReplay: j.idempotent_replay === true
|
|
24576
24583
|
};
|
|
24577
24584
|
}
|
|
24585
|
+
/** One device-wide delivery-ping socket per transport (P-5): the backend
|
|
24586
|
+
* supersedes a second socket for the same device, so every subscriber shares
|
|
24587
|
+
* one `openDeliveryPings` handle. Each gets its own bounded queue; its close()
|
|
24588
|
+
* detaches only itself; the socket closes with the last subscriber and a later
|
|
24589
|
+
* subscribe opens a fresh one. After a terminal close the transport is
|
|
24590
|
+
* terminal: later calls open no socket and get an already-ended iterator. */
|
|
24578
24591
|
subscribeDeliveryPings(_channelId) {
|
|
24592
|
+
if (this.pingsTerminal) {
|
|
24593
|
+
const ended = new PingSubscriber();
|
|
24594
|
+
ended.end();
|
|
24595
|
+
return { pings: ended.pings, close: () => {
|
|
24596
|
+
}, send: () => {
|
|
24597
|
+
} };
|
|
24598
|
+
}
|
|
24599
|
+
const hub = this.pingHub ??= this.openPingHub();
|
|
24600
|
+
const sub = hub.attach();
|
|
24601
|
+
return { pings: sub.pings, close: () => hub.detach(sub), send: hub.send };
|
|
24602
|
+
}
|
|
24603
|
+
openPingHub() {
|
|
24579
24604
|
let wsUrl = this.opts.baseUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/api/v1/ws";
|
|
24580
24605
|
if (this.opts.connectIdentityId) {
|
|
24581
24606
|
wsUrl += `?connect_identity_id=${encodeURIComponent(this.opts.connectIdentityId)}`;
|
|
24582
24607
|
}
|
|
24583
|
-
const
|
|
24608
|
+
const hub = new PingHub(openDeliveryPings({
|
|
24584
24609
|
url: wsUrl,
|
|
24585
24610
|
tokenProvider: this.tokenProvider,
|
|
24586
24611
|
wsImpl: this.ws,
|
|
24587
24612
|
hardened: this.opts.hardenedReconnect ?? false,
|
|
24588
|
-
onLifecycle: this.opts.onWsLifecycle
|
|
24589
|
-
|
|
24590
|
-
|
|
24591
|
-
|
|
24592
|
-
|
|
24593
|
-
this.
|
|
24594
|
-
|
|
24595
|
-
|
|
24596
|
-
|
|
24613
|
+
onLifecycle: this.opts.onWsLifecycle,
|
|
24614
|
+
// Any terminal close (4001, 4006, 4409, hardened war-cap) of the shared
|
|
24615
|
+
// socket makes this transport terminal. The shared iterator ends right
|
|
24616
|
+
// after this call, so the pump exit tears down every subscriber.
|
|
24617
|
+
onTerminalClose: (close) => {
|
|
24618
|
+
this.pingsTerminal = true;
|
|
24619
|
+
this.opts.onTerminalClose?.(close);
|
|
24620
|
+
}
|
|
24621
|
+
}), () => this.dropPingHub(hub));
|
|
24622
|
+
this._controlSend = hub.send;
|
|
24623
|
+
return hub;
|
|
24624
|
+
}
|
|
24625
|
+
dropPingHub(hub) {
|
|
24626
|
+
if (this.pingHub === hub) {
|
|
24627
|
+
this.pingHub = null;
|
|
24628
|
+
this._controlSend = null;
|
|
24629
|
+
}
|
|
24630
|
+
hub.shutdown();
|
|
24597
24631
|
}
|
|
24598
24632
|
sendControlFrame(_channelId, frame) {
|
|
24599
24633
|
try {
|
|
@@ -24621,6 +24655,90 @@ var RestTransport = class {
|
|
|
24621
24655
|
}));
|
|
24622
24656
|
}
|
|
24623
24657
|
};
|
|
24658
|
+
var SUBSCRIBER_QUEUE_MAX = 8;
|
|
24659
|
+
var PingSubscriber = class {
|
|
24660
|
+
queue = [];
|
|
24661
|
+
waiter;
|
|
24662
|
+
ended = false;
|
|
24663
|
+
started = false;
|
|
24664
|
+
pings = {
|
|
24665
|
+
[Symbol.asyncIterator]: () => {
|
|
24666
|
+
if (this.started)
|
|
24667
|
+
throw new Error("subscribeDeliveryPings: single-use async iterable");
|
|
24668
|
+
this.started = true;
|
|
24669
|
+
return {
|
|
24670
|
+
next: () => {
|
|
24671
|
+
if (this.ended)
|
|
24672
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
24673
|
+
if (this.queue.length)
|
|
24674
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
24675
|
+
return new Promise((resolve) => {
|
|
24676
|
+
this.waiter = resolve;
|
|
24677
|
+
});
|
|
24678
|
+
}
|
|
24679
|
+
};
|
|
24680
|
+
}
|
|
24681
|
+
};
|
|
24682
|
+
push(p) {
|
|
24683
|
+
if (this.ended)
|
|
24684
|
+
return;
|
|
24685
|
+
if (this.waiter) {
|
|
24686
|
+
const w = this.waiter;
|
|
24687
|
+
this.waiter = void 0;
|
|
24688
|
+
w({ value: p, done: false });
|
|
24689
|
+
} else if (this.queue.length < SUBSCRIBER_QUEUE_MAX) {
|
|
24690
|
+
this.queue.push(p);
|
|
24691
|
+
}
|
|
24692
|
+
}
|
|
24693
|
+
end() {
|
|
24694
|
+
this.ended = true;
|
|
24695
|
+
this.queue.length = 0;
|
|
24696
|
+
if (this.waiter) {
|
|
24697
|
+
const w = this.waiter;
|
|
24698
|
+
this.waiter = void 0;
|
|
24699
|
+
w({ value: void 0, done: true });
|
|
24700
|
+
}
|
|
24701
|
+
}
|
|
24702
|
+
};
|
|
24703
|
+
var PingHub = class {
|
|
24704
|
+
handle;
|
|
24705
|
+
onGone;
|
|
24706
|
+
subs = /* @__PURE__ */ new Set();
|
|
24707
|
+
send;
|
|
24708
|
+
constructor(handle, onGone) {
|
|
24709
|
+
this.handle = handle;
|
|
24710
|
+
this.onGone = onGone;
|
|
24711
|
+
this.send = handle.send;
|
|
24712
|
+
void this.pump();
|
|
24713
|
+
}
|
|
24714
|
+
/** `openDeliveryPings` iterables never reject and push() never throws, so
|
|
24715
|
+
* this loop only ends when the shared stream ends (our close, or terminal). */
|
|
24716
|
+
async pump() {
|
|
24717
|
+
for await (const p of this.handle.pings)
|
|
24718
|
+
for (const s of this.subs)
|
|
24719
|
+
s.push(p);
|
|
24720
|
+
this.onGone();
|
|
24721
|
+
}
|
|
24722
|
+
attach() {
|
|
24723
|
+
const s = new PingSubscriber();
|
|
24724
|
+
this.subs.add(s);
|
|
24725
|
+
return s;
|
|
24726
|
+
}
|
|
24727
|
+
/** Idempotent: detaches and ends only `s`; the last one out closes the socket. */
|
|
24728
|
+
detach(s) {
|
|
24729
|
+
this.subs.delete(s);
|
|
24730
|
+
s.end();
|
|
24731
|
+
if (this.subs.size === 0)
|
|
24732
|
+
this.onGone();
|
|
24733
|
+
}
|
|
24734
|
+
/** Idempotent: `openDeliveryPings().close()` is, and the set is empty after. */
|
|
24735
|
+
shutdown() {
|
|
24736
|
+
for (const s of this.subs)
|
|
24737
|
+
s.end();
|
|
24738
|
+
this.subs.clear();
|
|
24739
|
+
this.handle.close();
|
|
24740
|
+
}
|
|
24741
|
+
};
|
|
24624
24742
|
|
|
24625
24743
|
// ../connect-sdk-ts/dist/mls/rest-backend.js
|
|
24626
24744
|
function describeType(v) {
|
|
@@ -25542,7 +25660,9 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25542
25660
|
epoch: p.epoch,
|
|
25543
25661
|
payload: bytesToHex22(canonical),
|
|
25544
25662
|
connect_channel_id: p.channelId,
|
|
25545
|
-
created_at: ""
|
|
25663
|
+
created_at: "",
|
|
25664
|
+
identity_signature: null
|
|
25665
|
+
// the envelope above is unsigned
|
|
25546
25666
|
});
|
|
25547
25667
|
}
|
|
25548
25668
|
}
|
|
@@ -25554,7 +25674,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25554
25674
|
// edge stores the canonical envelope bytes (hex) and fans out one delivery row
|
|
25555
25675
|
// per active member device except the sender's. We resolve the sender's device
|
|
25556
25676
|
// from sender_id (recorded at registerDevice); if unknown, every member gets it.
|
|
25557
|
-
|
|
25677
|
+
//
|
|
25678
|
+
// Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
|
|
25679
|
+
// the envelope's `identity_proof.signature` as `identity_signature` beside the
|
|
25680
|
+
// unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
|
|
25681
|
+
async ingestApplication(envelope, opts) {
|
|
25558
25682
|
const channelId = envelope.channel_id;
|
|
25559
25683
|
const canonical = await canonicalEnvelopeBytes(envelope);
|
|
25560
25684
|
const members = this.membersByChannel.get(channelId) ?? [];
|
|
@@ -25572,7 +25696,8 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25572
25696
|
epoch: envelope.mls_group_epoch,
|
|
25573
25697
|
payload: bytesToHex22(canonical),
|
|
25574
25698
|
connect_channel_id: channelId,
|
|
25575
|
-
created_at: ""
|
|
25699
|
+
created_at: "",
|
|
25700
|
+
identity_signature: opts?.omitSignature ? null : envelope.identity_proof.signature || null
|
|
25576
25701
|
});
|
|
25577
25702
|
}
|
|
25578
25703
|
}
|
|
@@ -25944,11 +26069,15 @@ function openSchema(db) {
|
|
|
25944
26069
|
ensureUnifiedDmOwner(db);
|
|
25945
26070
|
ensureConversationLastAbsentAt(db);
|
|
25946
26071
|
ensureRecallForgetTables(db);
|
|
26072
|
+
ensureAgentMemoryOrigin(db);
|
|
25947
26073
|
ensureRuntimeInboxDeliveryAttempts(db);
|
|
25948
26074
|
ensureRuntimeReplyIntent(db);
|
|
25949
26075
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
25950
26076
|
ensureGroupLink(db);
|
|
25951
26077
|
ensureMlsCustody(db);
|
|
26078
|
+
ensureToolGrantTables(db);
|
|
26079
|
+
ensureToolGrantAppliedTable(db);
|
|
26080
|
+
ensureRoomSearchCapTable(db);
|
|
25952
26081
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
25953
26082
|
}
|
|
25954
26083
|
function ensureMlsCustody(db) {
|
|
@@ -26043,6 +26172,11 @@ function ensureRecallStateColumns(db) {
|
|
|
26043
26172
|
}
|
|
26044
26173
|
}
|
|
26045
26174
|
}
|
|
26175
|
+
function ensureAgentMemoryOrigin(db) {
|
|
26176
|
+
const cols = db.prepare("PRAGMA table_info(agent_memory)").all();
|
|
26177
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "origin"))
|
|
26178
|
+
db.exec("ALTER TABLE agent_memory ADD COLUMN origin TEXT");
|
|
26179
|
+
}
|
|
26046
26180
|
function ensureRecallForgetTables(db) {
|
|
26047
26181
|
db.exec(`
|
|
26048
26182
|
CREATE TABLE IF NOT EXISTS agent_memory_recall_forgotten (
|
|
@@ -26118,6 +26252,67 @@ function ensureUnifiedStateColumns(db) {
|
|
|
26118
26252
|
}
|
|
26119
26253
|
}
|
|
26120
26254
|
}
|
|
26255
|
+
function ensureToolGrantAppliedTable(db) {
|
|
26256
|
+
db.exec(`
|
|
26257
|
+
CREATE TABLE IF NOT EXISTS tool_grant_applied (
|
|
26258
|
+
identity_id TEXT NOT NULL,
|
|
26259
|
+
pin_key_b64 TEXT NOT NULL,
|
|
26260
|
+
sealed_applied BLOB NOT NULL,
|
|
26261
|
+
updated_at INTEGER NOT NULL,
|
|
26262
|
+
PRIMARY KEY (identity_id, pin_key_b64)
|
|
26263
|
+
);
|
|
26264
|
+
`);
|
|
26265
|
+
}
|
|
26266
|
+
function ensureRoomSearchCapTable(db) {
|
|
26267
|
+
db.exec(`
|
|
26268
|
+
CREATE TABLE IF NOT EXISTS room_search_cap (
|
|
26269
|
+
identity_id TEXT NOT NULL,
|
|
26270
|
+
channel_id TEXT NOT NULL,
|
|
26271
|
+
utc_day TEXT NOT NULL,
|
|
26272
|
+
granted_turns INTEGER NOT NULL,
|
|
26273
|
+
reached_by TEXT,
|
|
26274
|
+
notice_posted INTEGER NOT NULL,
|
|
26275
|
+
PRIMARY KEY (identity_id, channel_id)
|
|
26276
|
+
);
|
|
26277
|
+
`);
|
|
26278
|
+
}
|
|
26279
|
+
function ensureToolGrantTables(db) {
|
|
26280
|
+
db.exec(`
|
|
26281
|
+
CREATE TABLE IF NOT EXISTS tool_grant_manifest (
|
|
26282
|
+
identity_id TEXT NOT NULL,
|
|
26283
|
+
pin_key_b64 TEXT NOT NULL,
|
|
26284
|
+
counter INTEGER NOT NULL,
|
|
26285
|
+
sealed_manifest BLOB NOT NULL,
|
|
26286
|
+
updated_at INTEGER NOT NULL,
|
|
26287
|
+
PRIMARY KEY (identity_id, pin_key_b64)
|
|
26288
|
+
);
|
|
26289
|
+
CREATE TABLE IF NOT EXISTS owner_replay (
|
|
26290
|
+
identity_id TEXT NOT NULL,
|
|
26291
|
+
channel_id TEXT NOT NULL,
|
|
26292
|
+
message_id TEXT NOT NULL,
|
|
26293
|
+
group_id TEXT NOT NULL,
|
|
26294
|
+
epoch INTEGER NOT NULL,
|
|
26295
|
+
processed_at INTEGER NOT NULL,
|
|
26296
|
+
PRIMARY KEY (identity_id, channel_id, message_id)
|
|
26297
|
+
);
|
|
26298
|
+
CREATE INDEX IF NOT EXISTS owner_replay_age ON owner_replay (identity_id, channel_id, processed_at);
|
|
26299
|
+
CREATE TABLE IF NOT EXISTS owner_verify_failed (
|
|
26300
|
+
identity_id TEXT NOT NULL,
|
|
26301
|
+
channel_id TEXT NOT NULL,
|
|
26302
|
+
message_id TEXT NOT NULL,
|
|
26303
|
+
failed_at INTEGER NOT NULL,
|
|
26304
|
+
PRIMARY KEY (identity_id, message_id)
|
|
26305
|
+
);
|
|
26306
|
+
CREATE TABLE IF NOT EXISTS owner_replay_closed_epoch (
|
|
26307
|
+
identity_id TEXT NOT NULL,
|
|
26308
|
+
channel_id TEXT NOT NULL,
|
|
26309
|
+
group_id TEXT NOT NULL,
|
|
26310
|
+
epoch INTEGER NOT NULL,
|
|
26311
|
+
closed_at INTEGER NOT NULL,
|
|
26312
|
+
PRIMARY KEY (identity_id, channel_id, group_id, epoch)
|
|
26313
|
+
);
|
|
26314
|
+
`);
|
|
26315
|
+
}
|
|
26121
26316
|
|
|
26122
26317
|
// ../connect-mls-store/dist/errors.js
|
|
26123
26318
|
var MlsStoreCorruptError = class extends Error {
|
|
@@ -26272,7 +26467,16 @@ function itemVecAad(identityId, f, modelId) {
|
|
|
26272
26467
|
return enc4.encode(["agent_memory_item_vec", identityId, f.scopeKind, f.scopeId, f.originId, String(f.chunkIndex), modelId].join(NUL));
|
|
26273
26468
|
}
|
|
26274
26469
|
function encodeItemRecord(r) {
|
|
26275
|
-
return enc4.encode(JSON.stringify({
|
|
26470
|
+
return enc4.encode(JSON.stringify({
|
|
26471
|
+
v: 1,
|
|
26472
|
+
t: r.text,
|
|
26473
|
+
s: r.source,
|
|
26474
|
+
tr: r.trust,
|
|
26475
|
+
...r.sourceRef !== void 0 ? { sr: r.sourceRef } : {},
|
|
26476
|
+
...r.chunks !== void 0 ? { c: r.chunks } : {},
|
|
26477
|
+
...r.role !== void 0 ? { ro: r.role } : {},
|
|
26478
|
+
...r.origin !== void 0 ? { or: r.origin } : {}
|
|
26479
|
+
}));
|
|
26276
26480
|
}
|
|
26277
26481
|
function decodeItemRecord(b) {
|
|
26278
26482
|
let o;
|
|
@@ -26283,7 +26487,7 @@ function decodeItemRecord(b) {
|
|
|
26283
26487
|
}
|
|
26284
26488
|
if (!o || typeof o !== "object" || Array.isArray(o))
|
|
26285
26489
|
return null;
|
|
26286
|
-
const { v, t, s, tr, sr, c } = o;
|
|
26490
|
+
const { v, t, s, tr, sr, c, ro, or } = o;
|
|
26287
26491
|
if (v !== 1 || typeof t !== "string" || typeof s !== "string" || !ITEM_SOURCES.includes(s))
|
|
26288
26492
|
return null;
|
|
26289
26493
|
if (tr !== "owner" && tr !== "reference")
|
|
@@ -26294,7 +26498,19 @@ function decodeItemRecord(b) {
|
|
|
26294
26498
|
return null;
|
|
26295
26499
|
if (c !== void 0 && (typeof c !== "number" || !Number.isSafeInteger(c) || c < 1))
|
|
26296
26500
|
return null;
|
|
26297
|
-
|
|
26501
|
+
if (ro !== void 0 && ro !== "peer" && ro !== "agent")
|
|
26502
|
+
return null;
|
|
26503
|
+
if (or !== void 0 && or !== "fast_path" && or !== "reply" && or !== "untrusted")
|
|
26504
|
+
return null;
|
|
26505
|
+
return {
|
|
26506
|
+
text: t,
|
|
26507
|
+
source: s,
|
|
26508
|
+
trust: tr,
|
|
26509
|
+
...sr !== void 0 ? { sourceRef: sr } : {},
|
|
26510
|
+
...c !== void 0 ? { chunks: c } : {},
|
|
26511
|
+
...ro !== void 0 ? { role: ro } : {},
|
|
26512
|
+
...or !== void 0 ? { origin: or } : {}
|
|
26513
|
+
};
|
|
26298
26514
|
}
|
|
26299
26515
|
var noteSourceRef = (conversationId, noteId) => `agent_memory_note:${conversationId}:${noteId}`;
|
|
26300
26516
|
var recallSourceRef = (conversationId, messageId) => `agent_memory_recall:${conversationId}:${messageId}`;
|
|
@@ -26338,6 +26554,14 @@ function bytesEqual(a, b) {
|
|
|
26338
26554
|
function sameTag(prior, tag) {
|
|
26339
26555
|
return prior !== null && prior.groupId === tag.groupId && prior.epoch === tag.epoch;
|
|
26340
26556
|
}
|
|
26557
|
+
function metaOf(r) {
|
|
26558
|
+
return {
|
|
26559
|
+
...r.message_id !== null ? { messageId: r.message_id } : {},
|
|
26560
|
+
modality: r.modality,
|
|
26561
|
+
...isOrigin(r.origin) ? { origin: r.origin } : {}
|
|
26562
|
+
};
|
|
26563
|
+
}
|
|
26564
|
+
var isOrigin = (v) => v === "fast_path" || v === "reply" || v === "untrusted";
|
|
26341
26565
|
var UTF8 = new TextDecoder();
|
|
26342
26566
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
26343
26567
|
var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
|
|
@@ -26386,6 +26610,8 @@ function cachePreparedStatements(db) {
|
|
|
26386
26610
|
return st;
|
|
26387
26611
|
};
|
|
26388
26612
|
}
|
|
26613
|
+
var OWNER_REPLAY_MAX_PER_CHANNEL = 1e4;
|
|
26614
|
+
var OWNER_REPLAY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26389
26615
|
var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
26390
26616
|
db;
|
|
26391
26617
|
id;
|
|
@@ -26759,7 +26985,13 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
26759
26985
|
* same message (the bridge's ingest stamp). The staged row is never
|
|
26760
26986
|
* rewritten: on a match the FIRST staging stays authoritative.
|
|
26761
26987
|
*/
|
|
26762
|
-
|
|
26988
|
+
/**
|
|
26989
|
+
* Owner-signed tool grants (P-3, plan 1 task 4): when `ownerReplay` is given,
|
|
26990
|
+
* the owner replay entry and the runtime_inbox row are written in ONE
|
|
26991
|
+
* BEGIN IMMEDIATE transaction, so a failed stage records no replay entry and a
|
|
26992
|
+
* recorded replay entry always has its staged message.
|
|
26993
|
+
*/
|
|
26994
|
+
async stageRuntimeInbox(messageId, payload, receivedAt = Date.now(), samePayload = bytesEqual, ownerReplay) {
|
|
26763
26995
|
const prior = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
|
|
26764
26996
|
if (prior) {
|
|
26765
26997
|
const priorPayload = await this.openRuntimeInboxPayload(prior.sealed_message);
|
|
@@ -26769,9 +27001,20 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
26769
27001
|
return false;
|
|
26770
27002
|
}
|
|
26771
27003
|
const sealed = await this.cipher.seal(payload);
|
|
26772
|
-
const info = this.
|
|
26773
|
-
|
|
26774
|
-
|
|
27004
|
+
const info = this.immediate(() => {
|
|
27005
|
+
if (ownerReplay) {
|
|
27006
|
+
this.db.prepare(`INSERT OR IGNORE INTO owner_replay
|
|
27007
|
+
(identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
27008
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, ownerReplay.channelId, messageId, ownerReplay.groupId, ownerReplay.epoch, ownerReplay.processedAt);
|
|
27009
|
+
}
|
|
27010
|
+
if (this.failNextRuntimeInboxInsert) {
|
|
27011
|
+
this.failNextRuntimeInboxInsert = false;
|
|
27012
|
+
throw new Error("runtime_inbox insert failed (test)");
|
|
27013
|
+
}
|
|
27014
|
+
return this.db.prepare(`INSERT OR IGNORE INTO runtime_inbox
|
|
27015
|
+
(identity_id, message_id, sealed_message, received_at)
|
|
27016
|
+
VALUES (?, ?, ?, ?)`).run(this.id, messageId, sealed, receivedAt);
|
|
27017
|
+
});
|
|
26775
27018
|
if (info.changes === 0) {
|
|
26776
27019
|
const raced = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
|
|
26777
27020
|
const racedPayload = await this.openRuntimeInboxPayload(raced.sealed_message);
|
|
@@ -27054,10 +27297,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27054
27297
|
return { seq: 0, deduped: false, dropped: true };
|
|
27055
27298
|
}
|
|
27056
27299
|
const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
|
|
27057
|
-
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
|
|
27300
|
+
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at, origin)
|
|
27058
27301
|
VALUES (?, ?,
|
|
27059
27302
|
(SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
|
|
27060
|
-
?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now);
|
|
27303
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now, isOrigin(input.origin) ? input.origin : null);
|
|
27061
27304
|
if (info.changes === 0) {
|
|
27062
27305
|
const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
|
|
27063
27306
|
return { seq: row2.seq, deduped: true };
|
|
@@ -27081,6 +27324,29 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27081
27324
|
facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
|
|
27082
27325
|
return { recent, facts };
|
|
27083
27326
|
}
|
|
27327
|
+
/** Owner grants plan 4 Task 16: message id, modality and origin of the given lines (clear columns only, no text). */
|
|
27328
|
+
memoryLineMeta(conversationId, seqs) {
|
|
27329
|
+
const out = /* @__PURE__ */ new Map();
|
|
27330
|
+
const stmt = this.db.prepare("SELECT message_id, modality, origin FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq=?");
|
|
27331
|
+
for (const seq of seqs) {
|
|
27332
|
+
const r = stmt.get(this.id, conversationId, seq);
|
|
27333
|
+
if (r)
|
|
27334
|
+
out.set(seq, metaOf(r));
|
|
27335
|
+
}
|
|
27336
|
+
return out;
|
|
27337
|
+
}
|
|
27338
|
+
/** Owner grants plan 4 Task 16 (audit r2 M-1): every peer row's message id in the conversation, not the 50-row window. */
|
|
27339
|
+
memoryPeerMessageIds(conversationId) {
|
|
27340
|
+
return this.db.prepare("SELECT message_id FROM agent_memory WHERE identity_id=? AND conversation_id=? AND role='peer' AND message_id IS NOT NULL").all(this.id, conversationId).map((r) => r.message_id);
|
|
27341
|
+
}
|
|
27342
|
+
/** Owner grants plan 4 Task 16 (OQ-9): the history row an imported_history item came from, or null when it is gone. */
|
|
27343
|
+
memoryRowForTrace(conversationId, at) {
|
|
27344
|
+
const cols = "role, peer_id, message_id, modality, origin, recorded_at";
|
|
27345
|
+
const r = at.seq !== void 0 ? this.db.prepare(`SELECT ${cols} FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq=?`).get(this.id, conversationId, at.seq) : at.messageId !== void 0 ? this.db.prepare(`SELECT ${cols} FROM agent_memory WHERE identity_id=? AND conversation_id=? AND message_id=?`).get(this.id, conversationId, at.messageId) : void 0;
|
|
27346
|
+
if (!r)
|
|
27347
|
+
return null;
|
|
27348
|
+
return { role: r.role === "agent" ? "agent" : "peer", ts: Number(r.recorded_at), ...r.peer_id ? { peerId: r.peer_id } : {}, ...metaOf(r) };
|
|
27349
|
+
}
|
|
27084
27350
|
async recordMemoryFact(conversationId, fact) {
|
|
27085
27351
|
const now = this.memoryNow();
|
|
27086
27352
|
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
@@ -28108,7 +28374,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28108
28374
|
}
|
|
28109
28375
|
/** §14.1 (R-U12): one conversation's history in seq order, opened under the EMPTY AAD. Batched by `afterSeq`; `next` is {at: recorded_at, id: seq}. */
|
|
28110
28376
|
async readHistoryForMigration(conversationId, sinceMs, afterSeq, limit) {
|
|
28111
|
-
const raw = this.db.prepare(`SELECT seq, message_id, role, sealed_text, recorded_at FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at>=? AND seq>?
|
|
28377
|
+
const raw = this.db.prepare(`SELECT seq, message_id, role, sealed_text, recorded_at, modality, origin FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at>=? AND seq>?
|
|
28112
28378
|
ORDER BY seq ASC LIMIT ?`).all(this.id, conversationId, sinceMs, afterSeq, limit);
|
|
28113
28379
|
const rows = [];
|
|
28114
28380
|
let unreadable = 0;
|
|
@@ -28117,7 +28383,15 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28117
28383
|
await yieldToLoop();
|
|
28118
28384
|
try {
|
|
28119
28385
|
const text = UTF8.decode(await this.cipher.open(r.sealed_text));
|
|
28120
|
-
rows.push({
|
|
28386
|
+
rows.push({
|
|
28387
|
+
seq: Number(r.seq),
|
|
28388
|
+
messageId: r.message_id ?? null,
|
|
28389
|
+
role: r.role === "agent" ? "agent" : "peer",
|
|
28390
|
+
text,
|
|
28391
|
+
recordedAt: Number(r.recorded_at),
|
|
28392
|
+
modality: r.modality,
|
|
28393
|
+
...isOrigin(r.origin) ? { origin: r.origin } : {}
|
|
28394
|
+
});
|
|
28121
28395
|
} catch {
|
|
28122
28396
|
unreadable++;
|
|
28123
28397
|
}
|
|
@@ -28519,6 +28793,156 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28519
28793
|
const res = expectCreatedAt === void 0 ? this.db.prepare("DELETE FROM mls_owed_followup WHERE identity_id=? AND channel_id=?").run(this.id, channelId) : this.db.prepare("DELETE FROM mls_owed_followup WHERE identity_id=? AND channel_id=? AND created_at=?").run(this.id, channelId, expectCreatedAt);
|
|
28520
28794
|
return Number(res.changes) > 0;
|
|
28521
28795
|
}
|
|
28796
|
+
// ---------------------------------------------------------------------------
|
|
28797
|
+
// Owner-signed tool grants, step 1 (plan 1 task 4; spec §7.3, §7.11; D-2).
|
|
28798
|
+
/** The last good manifest wrapper stored for this pinned public key, or null. */
|
|
28799
|
+
async loadToolGrantManifest(pinKeyB64) {
|
|
28800
|
+
const row = this.db.prepare("SELECT counter, sealed_manifest FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
|
|
28801
|
+
if (!row)
|
|
28802
|
+
return null;
|
|
28803
|
+
return { counter: Number(row.counter), wrapper: await this.unsealJson(row.sealed_manifest) };
|
|
28804
|
+
}
|
|
28805
|
+
/** Stores a verified wrapper. The counter can never fall or repeat, whatever the
|
|
28806
|
+
* caller does: the upsert's WHERE refuses it and this throws. */
|
|
28807
|
+
async saveToolGrantManifest(pinKeyB64, counter, wrapper) {
|
|
28808
|
+
const sealed = await this.sealJson(wrapper);
|
|
28809
|
+
const info = this.db.prepare(`INSERT INTO tool_grant_manifest (identity_id, pin_key_b64, counter, sealed_manifest, updated_at)
|
|
28810
|
+
VALUES (?, ?, ?, ?, ?)
|
|
28811
|
+
ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
|
|
28812
|
+
counter = excluded.counter, sealed_manifest = excluded.sealed_manifest, updated_at = excluded.updated_at
|
|
28813
|
+
WHERE excluded.counter > tool_grant_manifest.counter`).run(this.id, pinKeyB64, counter, sealed, Date.now());
|
|
28814
|
+
if (Number(info.changes) === 0)
|
|
28815
|
+
throw new Error("tool grant counter not increasing");
|
|
28816
|
+
}
|
|
28817
|
+
/** Counter rule §6.6: state is per pinned key; a new key starts a new counter. */
|
|
28818
|
+
dropToolGrantManifestsExcept(pinKeyB64) {
|
|
28819
|
+
return Number(this.db.prepare("DELETE FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
28820
|
+
}
|
|
28821
|
+
// Owner-signed tool grants plan 4, Task 12 (spec §7.5): the room web_search backstop, per room per UTC day.
|
|
28822
|
+
/** A row of another day reads as zero. */
|
|
28823
|
+
roomSearchCapState(channelId, utcDay) {
|
|
28824
|
+
const row = this.db.prepare("SELECT granted_turns, reached_by, notice_posted FROM room_search_cap WHERE identity_id=? AND channel_id=? AND utc_day=?").get(this.id, channelId, utcDay);
|
|
28825
|
+
if (!row)
|
|
28826
|
+
return { grantedTurns: 0, reachedBy: null, noticePosted: false };
|
|
28827
|
+
const by = row.reached_by === "bridge" || row.reached_by === "runtime" ? row.reached_by : null;
|
|
28828
|
+
return { grantedTurns: Number(row.granted_turns), reachedBy: by, noticePosted: Number(row.notice_posted) === 1 };
|
|
28829
|
+
}
|
|
28830
|
+
/** One statement: a row of an earlier day is reset, then +1. Returns the new count. */
|
|
28831
|
+
countRoomSearchGrant(channelId, utcDay) {
|
|
28832
|
+
const row = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
|
|
28833
|
+
VALUES (?, ?, ?, 1, NULL, 0)
|
|
28834
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET
|
|
28835
|
+
granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns + 1 ELSE 1 END,
|
|
28836
|
+
reached_by = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.reached_by ELSE NULL END,
|
|
28837
|
+
notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
|
|
28838
|
+
utc_day = excluded.utc_day
|
|
28839
|
+
RETURNING granted_turns`).get(this.id, channelId, utcDay);
|
|
28840
|
+
return Number(row.granted_turns);
|
|
28841
|
+
}
|
|
28842
|
+
/** True when this call set the flag (the first time today). */
|
|
28843
|
+
markRoomSearchCapReached(channelId, utcDay, by) {
|
|
28844
|
+
const info = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
|
|
28845
|
+
VALUES (?, ?, ?, 0, ?, 0)
|
|
28846
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET
|
|
28847
|
+
granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns ELSE 0 END,
|
|
28848
|
+
notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
|
|
28849
|
+
reached_by = excluded.reached_by,
|
|
28850
|
+
utc_day = excluded.utc_day
|
|
28851
|
+
WHERE room_search_cap.utc_day <> excluded.utc_day OR room_search_cap.reached_by IS NULL`).run(this.id, channelId, utcDay, by);
|
|
28852
|
+
return Number(info.changes) > 0;
|
|
28853
|
+
}
|
|
28854
|
+
markRoomSearchCapNoticePosted(channelId, utcDay) {
|
|
28855
|
+
this.db.prepare("UPDATE room_search_cap SET notice_posted = 1 WHERE identity_id=? AND channel_id=? AND utc_day=?").run(this.id, channelId, utcDay);
|
|
28856
|
+
}
|
|
28857
|
+
/** Deletes rows of earlier days; called at start(). */
|
|
28858
|
+
pruneRoomSearchCap(utcDay) {
|
|
28859
|
+
return Number(this.db.prepare("DELETE FROM room_search_cap WHERE identity_id=? AND utc_day < ?").run(this.id, utcDay).changes);
|
|
28860
|
+
}
|
|
28861
|
+
// Owner-signed tool grants plan 4, Task 11 (rulings item 3): the last applied good search block.
|
|
28862
|
+
/** The applied record for this pinned key, or null. `search: null` means "applied with no search block". */
|
|
28863
|
+
async loadToolGrantApplied(pinKeyB64) {
|
|
28864
|
+
const row = this.db.prepare("SELECT sealed_applied FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
|
|
28865
|
+
if (!row)
|
|
28866
|
+
return null;
|
|
28867
|
+
return await this.unsealJson(row.sealed_applied);
|
|
28868
|
+
}
|
|
28869
|
+
async saveToolGrantApplied(pinKeyB64, applied) {
|
|
28870
|
+
const sealed = await this.sealJson({ counter: applied.counter, search: applied.search });
|
|
28871
|
+
this.db.prepare(`INSERT INTO tool_grant_applied (identity_id, pin_key_b64, sealed_applied, updated_at)
|
|
28872
|
+
VALUES (?, ?, ?, ?)
|
|
28873
|
+
ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
|
|
28874
|
+
sealed_applied = excluded.sealed_applied, updated_at = excluded.updated_at`).run(this.id, pinKeyB64, sealed, Date.now());
|
|
28875
|
+
}
|
|
28876
|
+
/** Counter rule §6.6: a new pinned key drops the old key's applied record. */
|
|
28877
|
+
dropToolGrantAppliedExcept(pinKeyB64) {
|
|
28878
|
+
return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
28879
|
+
}
|
|
28880
|
+
hasOwnerMessage(channelId, messageId) {
|
|
28881
|
+
return this.db.prepare("SELECT 1 FROM owner_replay WHERE identity_id=? AND channel_id=? AND message_id=?").get(this.id, channelId, messageId) !== void 0;
|
|
28882
|
+
}
|
|
28883
|
+
isOwnerEpochClosed(channelId, groupId, epoch) {
|
|
28884
|
+
return this.db.prepare("SELECT 1 FROM owner_replay_closed_epoch WHERE identity_id=? AND channel_id=? AND group_id=? AND epoch=?").get(this.id, channelId, groupId, epoch) !== void 0;
|
|
28885
|
+
}
|
|
28886
|
+
recordOwnerVerifyFailure(channelId, messageId, failedAt = Date.now()) {
|
|
28887
|
+
this.db.prepare("INSERT OR IGNORE INTO owner_verify_failed (identity_id, channel_id, message_id, failed_at) VALUES (?, ?, ?, ?)").run(this.id, channelId, messageId, failedAt);
|
|
28888
|
+
}
|
|
28889
|
+
isOwnerVerifyFailed(messageId) {
|
|
28890
|
+
return this.db.prepare("SELECT 1 FROM owner_verify_failed WHERE identity_id=? AND message_id=?").get(this.id, messageId) !== void 0;
|
|
28891
|
+
}
|
|
28892
|
+
listOwnerReplayChannels() {
|
|
28893
|
+
return this.db.prepare("SELECT DISTINCT channel_id FROM owner_replay WHERE identity_id=? ORDER BY channel_id").all(this.id).map((r) => r.channel_id);
|
|
28894
|
+
}
|
|
28895
|
+
/**
|
|
28896
|
+
* Decision D-2. Per channel with a known current (group, epoch): rows of the
|
|
28897
|
+
* current pair are never pruned; among the others, drop rows older than
|
|
28898
|
+
* `maxAgeMs`, then every row past the newest `maxPerChannel`, oldest first.
|
|
28899
|
+
* Every pair that lost a row is recorded closed. A channel with no current
|
|
28900
|
+
* position is skipped this pass. Old `owner_verify_failed` rows go too.
|
|
28901
|
+
* Returns the number of owner_replay rows removed.
|
|
28902
|
+
*/
|
|
28903
|
+
pruneOwnerReplay(nowMs, currentOf, opts = {}) {
|
|
28904
|
+
const maxPerChannel = opts.maxPerChannel ?? OWNER_REPLAY_MAX_PER_CHANNEL;
|
|
28905
|
+
const maxAgeMs = opts.maxAgeMs ?? OWNER_REPLAY_MAX_AGE_MS;
|
|
28906
|
+
const cutoff = nowMs - maxAgeMs;
|
|
28907
|
+
return this.immediate(() => {
|
|
28908
|
+
let removed = 0;
|
|
28909
|
+
for (const channelId of this.listOwnerReplayChannels()) {
|
|
28910
|
+
const cur = currentOf.get(channelId);
|
|
28911
|
+
if (!cur)
|
|
28912
|
+
continue;
|
|
28913
|
+
const others = this.db.prepare(`SELECT rowid AS rid, group_id, epoch, processed_at FROM owner_replay
|
|
28914
|
+
WHERE identity_id=? AND channel_id=? AND NOT (group_id=? AND epoch=?)
|
|
28915
|
+
ORDER BY processed_at DESC, rowid DESC`).all(this.id, channelId, cur.groupId, cur.epoch);
|
|
28916
|
+
const young = others.filter((r) => Number(r.processed_at) >= cutoff);
|
|
28917
|
+
const victims = [...others.filter((r) => Number(r.processed_at) < cutoff), ...young.slice(maxPerChannel)];
|
|
28918
|
+
const closed = /* @__PURE__ */ new Map();
|
|
28919
|
+
const del = this.db.prepare("DELETE FROM owner_replay WHERE rowid=?");
|
|
28920
|
+
for (const v of victims) {
|
|
28921
|
+
removed += Number(del.run(v.rid).changes);
|
|
28922
|
+
closed.set(`${v.group_id}\0${v.epoch}`, { groupId: v.group_id, epoch: Number(v.epoch) });
|
|
28923
|
+
}
|
|
28924
|
+
const close = this.db.prepare(`INSERT OR IGNORE INTO owner_replay_closed_epoch (identity_id, channel_id, group_id, epoch, closed_at)
|
|
28925
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
28926
|
+
for (const p of closed.values()) {
|
|
28927
|
+
if (p.groupId === cur.groupId && p.epoch === cur.epoch)
|
|
28928
|
+
continue;
|
|
28929
|
+
close.run(this.id, channelId, p.groupId, p.epoch, nowMs);
|
|
28930
|
+
}
|
|
28931
|
+
}
|
|
28932
|
+
this.db.prepare("DELETE FROM owner_verify_failed WHERE identity_id=? AND failed_at < ?").run(this.id, cutoff);
|
|
28933
|
+
return removed;
|
|
28934
|
+
});
|
|
28935
|
+
}
|
|
28936
|
+
failNextRuntimeInboxInsert = false;
|
|
28937
|
+
/** @internal test-only: the next runtime_inbox INSERT inside the staging transaction throws once. */
|
|
28938
|
+
_failNextRuntimeInboxInsertForTest() {
|
|
28939
|
+
this.failNextRuntimeInboxInsert = true;
|
|
28940
|
+
}
|
|
28941
|
+
/** @internal test-only */
|
|
28942
|
+
_insertOwnerReplayForTest(channelId, messageId, groupId, epoch, processedAt) {
|
|
28943
|
+
this.db.prepare(`INSERT INTO owner_replay (identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
28944
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
|
|
28945
|
+
}
|
|
28522
28946
|
/** @internal test-only */
|
|
28523
28947
|
_rawMainForTest(identityId, channelId) {
|
|
28524
28948
|
const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
|
|
@@ -28671,8 +29095,8 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
28671
29095
|
}
|
|
28672
29096
|
|
|
28673
29097
|
// src/bridge-agent.ts
|
|
28674
|
-
import { promises as fsp, statSync as statSync4 } from "node:fs";
|
|
28675
|
-
import { join as
|
|
29098
|
+
import { existsSync as existsSync5, promises as fsp, statSync as statSync4 } from "node:fs";
|
|
29099
|
+
import { join as join17, dirname as dirname5 } from "node:path";
|
|
28676
29100
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28677
29101
|
|
|
28678
29102
|
// src/latency.ts
|
|
@@ -29263,8 +29687,8 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
|
|
|
29263
29687
|
async function materializeAttachment(body, ctx, deps) {
|
|
29264
29688
|
await deps.ready();
|
|
29265
29689
|
const ciphertext = await deps.downloadBlob(body.blobId);
|
|
29266
|
-
const
|
|
29267
|
-
if (
|
|
29690
|
+
const sha25612 = await deps.computeFileDigest(ciphertext);
|
|
29691
|
+
if (sha25612 !== body.digest) {
|
|
29268
29692
|
throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
|
|
29269
29693
|
}
|
|
29270
29694
|
const plain = await deps.decryptFile(
|
|
@@ -29277,7 +29701,7 @@ async function materializeAttachment(body, ctx, deps) {
|
|
|
29277
29701
|
const path = `${dir}/${ctx.messageId}-${safeName}`;
|
|
29278
29702
|
await deps.mkdirp(dir);
|
|
29279
29703
|
await deps.writeFile(path, plain);
|
|
29280
|
-
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256:
|
|
29704
|
+
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha25612 };
|
|
29281
29705
|
}
|
|
29282
29706
|
|
|
29283
29707
|
// src/sodium-ready.ts
|
|
@@ -29866,11 +30290,11 @@ function quoteFromMessage(note, record) {
|
|
|
29866
30290
|
}
|
|
29867
30291
|
function renderNotes(notes) {
|
|
29868
30292
|
if (notes.length === 0) return void 0;
|
|
29869
|
-
const
|
|
30293
|
+
const lines2 = notes.map((n) => {
|
|
29870
30294
|
const one = oneLine(n.text);
|
|
29871
30295
|
return `- [n${n.id}] ${/^[#\-\[]/.test(one) ? `\\${one}` : one}`;
|
|
29872
30296
|
});
|
|
29873
|
-
return [NOTES_HEADING, ...
|
|
30297
|
+
return [NOTES_HEADING, ...lines2].join("\n");
|
|
29874
30298
|
}
|
|
29875
30299
|
|
|
29876
30300
|
// src/memory/secret-scan.ts
|
|
@@ -30619,7 +31043,7 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
|
|
|
30619
31043
|
|
|
30620
31044
|
// src/memory/embedder/manifest.ts
|
|
30621
31045
|
var MODEL_ORIGIN = "https://downloads.securecoms.ai";
|
|
30622
|
-
var file = (modelId, name,
|
|
31046
|
+
var file = (modelId, name, sha25612, size) => Object.freeze({ name, urlPath: `/models/embeddings/${modelId}/${sha25612}/${name}`, sha256: sha25612, size });
|
|
30623
31047
|
var MODEL_ID = "minilm-l6-v2-int8";
|
|
30624
31048
|
var EMBEDDING_MANIFEST = Object.freeze({
|
|
30625
31049
|
modelId: MODEL_ID,
|
|
@@ -30963,8 +31387,8 @@ import { join as join11, win32 } from "node:path";
|
|
|
30963
31387
|
var WINDOWS_SEMANTIC_ENABLED = false;
|
|
30964
31388
|
var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
|
|
30965
31389
|
function parseSaveSddl(saveFile) {
|
|
30966
|
-
const
|
|
30967
|
-
return (
|
|
31390
|
+
const lines2 = saveFile.toString("utf16le").replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
31391
|
+
return (lines2[1] ?? "").trim();
|
|
30968
31392
|
}
|
|
30969
31393
|
function aclAllows(sddl, ownerSid) {
|
|
30970
31394
|
const allowed = /* @__PURE__ */ new Set([ownerSid, "SY", "BA"]);
|
|
@@ -32359,7 +32783,15 @@ var ItemBook = class {
|
|
|
32359
32783
|
originAt: r.originAt,
|
|
32360
32784
|
storedAt
|
|
32361
32785
|
});
|
|
32362
|
-
const rec = (text, chunks) => ({
|
|
32786
|
+
const rec = (text, chunks) => ({
|
|
32787
|
+
text,
|
|
32788
|
+
source: r.source,
|
|
32789
|
+
trust: r.trust,
|
|
32790
|
+
...r.sourceRef !== void 0 ? { sourceRef: r.sourceRef } : {},
|
|
32791
|
+
...chunks ? { chunks } : {},
|
|
32792
|
+
...r.role !== void 0 ? { role: r.role } : {},
|
|
32793
|
+
...r.origin !== void 0 ? { origin: r.origin } : {}
|
|
32794
|
+
});
|
|
32363
32795
|
if (chunked) parts.push({ itemId: first, chunkIndex: 0, parentId: null, parentOriginId: "", sealedRecord: await this.d.store.sealItemRecord(fields(0), rec("", texts.length)), sealedVec: null, modelId: null, tokenMacs: [], embeddable: false });
|
|
32364
32796
|
const inlineOk = r.pendingOnly !== true && !this.d.paused() && this.d.embedOn() && this.d.embedder.state().state === "ready";
|
|
32365
32797
|
for (const [i, text] of texts.entries()) {
|
|
@@ -32638,7 +33070,7 @@ var UnifiedReadBook = class {
|
|
|
32638
33070
|
this.entries.set(p.messageId, { channelId: p.channelId, scopeId: p.scopeId, capture, pins: pins?.pins ?? [], pinnedMore: pins?.more ?? 0, items });
|
|
32639
33071
|
}
|
|
32640
33072
|
/** ws flush (synchronous): the entry's own channel and owner scope, its capture still current (H-4, U11). */
|
|
32641
|
-
deliverable(messageId, channelId, scopeIdNow) {
|
|
33073
|
+
deliverable(messageId, channelId, scopeIdNow, keep = () => true) {
|
|
32642
33074
|
const e = this.entries.get(messageId);
|
|
32643
33075
|
if (!e || e.channelId !== channelId || e.scopeId !== scopeIdNow) return void 0;
|
|
32644
33076
|
if (!same(e.capture, this.d.store.itemCapture(channelId))) {
|
|
@@ -32647,8 +33079,9 @@ var UnifiedReadBook = class {
|
|
|
32647
33079
|
}
|
|
32648
33080
|
const at = (i) => ({ id: i.itemId, source: i.source, at: i.originAt, text: clampText(i.text) });
|
|
32649
33081
|
const all = [
|
|
32650
|
-
|
|
32651
|
-
...e.
|
|
33082
|
+
// Owner grants plan 4 Task 16: `keep` is the voice fence on a pinned agent (never a SQL read here; the bridge supplies it).
|
|
33083
|
+
...e.pins.filter(keep).map((i) => ({ block: "pinned", ...at(i) })),
|
|
33084
|
+
...e.items.filter(keep).map((i) => ({ block: i.trust === "owner" ? "owner" : "reference", ...at(i) }))
|
|
32652
33085
|
];
|
|
32653
33086
|
const items = all.slice(0, FRAME_ITEMS_MAX);
|
|
32654
33087
|
const pinsCut = Math.max(0, e.pins.length - FRAME_ITEMS_MAX);
|
|
@@ -32693,6 +33126,56 @@ var UnifiedReadBook = class {
|
|
|
32693
33126
|
}
|
|
32694
33127
|
};
|
|
32695
33128
|
|
|
33129
|
+
// src/grants/voice-fence.ts
|
|
33130
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
33131
|
+
var FAST_ID_EPOCH_MS = 17887392e5;
|
|
33132
|
+
function legacyFastReplyId(messageId) {
|
|
33133
|
+
const h = createHash4("sha256").update(`fast:${messageId}`).digest();
|
|
33134
|
+
h[6] = h[6] & 15 | 64;
|
|
33135
|
+
h[8] = h[8] & 63 | 128;
|
|
33136
|
+
const x = h.subarray(0, 16).toString("hex");
|
|
33137
|
+
return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
|
|
33138
|
+
}
|
|
33139
|
+
function classifyAgentLine(line, ctx) {
|
|
33140
|
+
if (line.origin === "fast_path" || line.origin === "reply" || line.origin === "untrusted") return line.origin;
|
|
33141
|
+
if (line.modality === "voice") return "fast_path";
|
|
33142
|
+
if (line.messageId !== void 0 && ctx.fastIds.has(line.messageId)) return "fast_path";
|
|
33143
|
+
if (line.ts !== void 0 && line.ts < FAST_ID_EPOCH_MS) return "fast_path";
|
|
33144
|
+
if (line.modality === "text") return "reply";
|
|
33145
|
+
return "fast_path";
|
|
33146
|
+
}
|
|
33147
|
+
function isRelayClassPeer(line, ownerId) {
|
|
33148
|
+
return line.modality === "voice" || line.peerId !== ownerId;
|
|
33149
|
+
}
|
|
33150
|
+
function parseHistoryRef(ref) {
|
|
33151
|
+
const m = /^agent_memory:(.+):(\d+)$/.exec(ref);
|
|
33152
|
+
return m ? { conversationId: m[1], seq: Number(m[2]) } : null;
|
|
33153
|
+
}
|
|
33154
|
+
function classifyUnifiedItem(item, trace, ctx, strict, keepUntrusted = false) {
|
|
33155
|
+
if (strict?.turnChannelId !== void 0 && item.channelId !== strict.turnChannelId) {
|
|
33156
|
+
if (item.source === "owner_typed") return "keep";
|
|
33157
|
+
if (item.source !== "imported_history") return "exclude";
|
|
33158
|
+
const row2 = traceItem(item, trace);
|
|
33159
|
+
return row2 !== null && row2.role === "peer" && !isRelayClassPeer(row2, strict.ownerId) ? "keep" : "exclude";
|
|
33160
|
+
}
|
|
33161
|
+
if (strict && item.source === "owner_voice") return "exclude";
|
|
33162
|
+
if (item.role === "peer" && !(strict && item.source === "imported_history")) return "keep";
|
|
33163
|
+
if (item.role !== "peer" && item.origin !== void 0) return item.origin === "reply" || keepUntrusted && item.origin === "untrusted" ? "keep" : "exclude";
|
|
33164
|
+
if (item.source === "agent_reply") return "keep";
|
|
33165
|
+
if (item.source !== "imported_history") return "keep";
|
|
33166
|
+
const row = traceItem(item, trace);
|
|
33167
|
+
if (row === null) return "exclude";
|
|
33168
|
+
if (row.role === "peer") return strict && isRelayClassPeer(row, strict.ownerId) ? "exclude" : "keep";
|
|
33169
|
+
const origin = classifyAgentLine(row, ctx);
|
|
33170
|
+
return origin === "reply" || keepUntrusted && origin === "untrusted" ? "keep" : "exclude";
|
|
33171
|
+
}
|
|
33172
|
+
function traceItem(item, trace) {
|
|
33173
|
+
const ref = item.sourceRef !== void 0 ? parseHistoryRef(item.sourceRef) : null;
|
|
33174
|
+
if (ref !== null) return trace(ref.conversationId, { seq: ref.seq });
|
|
33175
|
+
const seqForm = /^seq:(\d+)$/.exec(item.originId);
|
|
33176
|
+
return seqForm ? trace(item.channelId, { seq: Number(seqForm[1]) }) : trace(item.channelId, { messageId: item.originId });
|
|
33177
|
+
}
|
|
33178
|
+
|
|
32696
33179
|
// src/memory/unified/migrate.ts
|
|
32697
33180
|
var OLD_TABLES_PRESENT = true;
|
|
32698
33181
|
var MIGRATION_BATCH = 100;
|
|
@@ -32845,7 +33328,10 @@ async function runMigration(d, o = {}) {
|
|
|
32845
33328
|
text: r.text,
|
|
32846
33329
|
sourceRef: historySourceRef(c, r.seq),
|
|
32847
33330
|
pendingOnly: true,
|
|
32848
|
-
expect
|
|
33331
|
+
expect,
|
|
33332
|
+
// Owner grants plan 4 Task 16: the row's role and, for an agent row, its classified origin.
|
|
33333
|
+
role: r.role,
|
|
33334
|
+
...r.role === "agent" ? { origin: d.classifyHistoryOrigin ? d.classifyHistoryOrigin(c, r) : classifyAgentLine({ origin: r.origin, modality: r.modality, ...r.messageId ? { messageId: r.messageId } : {}, ts: r.recordedAt }, { fastIds: /* @__PURE__ */ new Set() }) } : {}
|
|
32849
33335
|
}),
|
|
32850
33336
|
(r) => r.recordedAt,
|
|
32851
33337
|
t
|
|
@@ -32903,9 +33389,9 @@ async function recordInboundMemory(sink, channelId, senderId, text, messageId, m
|
|
|
32903
33389
|
} catch {
|
|
32904
33390
|
}
|
|
32905
33391
|
}
|
|
32906
|
-
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch) {
|
|
33392
|
+
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch, origin) {
|
|
32907
33393
|
try {
|
|
32908
|
-
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch } });
|
|
33394
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch }, ...origin ? { origin } : {} });
|
|
32909
33395
|
} catch {
|
|
32910
33396
|
}
|
|
32911
33397
|
}
|
|
@@ -33261,10 +33747,10 @@ function splitFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
33261
33747
|
return { reply: normalizeFastReply(raw.replace(DEFER_RE, " "), maxChars, maxSentences), defer };
|
|
33262
33748
|
}
|
|
33263
33749
|
function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
33264
|
-
const
|
|
33750
|
+
const lines2 = raw.split(/\r?\n+/).map(
|
|
33265
33751
|
(l) => l.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").replace(/^\s*#{1,6}\s+/, "").replace(/[*_`]+/g, "").trim()
|
|
33266
33752
|
).filter(Boolean);
|
|
33267
|
-
const sentences =
|
|
33753
|
+
const sentences = lines2.flatMap((l) => l.split(SENTENCE_END)).map((s) => s.trim()).filter(Boolean);
|
|
33268
33754
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
33269
33755
|
}
|
|
33270
33756
|
|
|
@@ -33476,18 +33962,18 @@ function parseSummarizeBody(parsed, selfIdentityId, log = () => {
|
|
|
33476
33962
|
function transcriptToPrompt(transcript) {
|
|
33477
33963
|
const names = /* @__PURE__ */ new Map();
|
|
33478
33964
|
for (const p of transcript.participants ?? []) names.set(p.identity_id, p.display_name);
|
|
33479
|
-
const
|
|
33965
|
+
const lines2 = transcript.turns.map(
|
|
33480
33966
|
(t) => `${names.get(t.speaker_identity_id) ?? t.speaker_identity_id}: ${t.text}`
|
|
33481
33967
|
);
|
|
33482
|
-
return `${SUMMARY_PROMPT}${
|
|
33968
|
+
return `${SUMMARY_PROMPT}${lines2.join("\n")}
|
|
33483
33969
|
`;
|
|
33484
33970
|
}
|
|
33485
33971
|
function parseSummaryOutput(text) {
|
|
33486
|
-
const
|
|
33487
|
-
const idx =
|
|
33972
|
+
const lines2 = text.split("\n");
|
|
33973
|
+
const idx = lines2.findIndex((line) => /^\s*Action items:\s*$/i.test(line));
|
|
33488
33974
|
if (idx < 0) return { paragraph: text.trim(), actionItems: [] };
|
|
33489
|
-
const paragraph =
|
|
33490
|
-
const actionItems =
|
|
33975
|
+
const paragraph = lines2.slice(0, idx).join("\n").trim();
|
|
33976
|
+
const actionItems = lines2.slice(idx + 1).map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").trim()).filter((line) => line.length > 0);
|
|
33491
33977
|
return { paragraph, actionItems };
|
|
33492
33978
|
}
|
|
33493
33979
|
function buildSummaryBody(request, out, now) {
|
|
@@ -33527,7 +34013,7 @@ async function runSummarize(opts) {
|
|
|
33527
34013
|
}
|
|
33528
34014
|
|
|
33529
34015
|
// src/service/runtime-config-dir.ts
|
|
33530
|
-
import { createHash as
|
|
34016
|
+
import { createHash as createHash5, randomUUID } from "node:crypto";
|
|
33531
34017
|
import {
|
|
33532
34018
|
chmodSync as chmodSync4,
|
|
33533
34019
|
existsSync as existsSync4,
|
|
@@ -33562,7 +34048,24 @@ function assertManagedName(name) {
|
|
|
33562
34048
|
}
|
|
33563
34049
|
function isLocalOnlyName(name) {
|
|
33564
34050
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
33565
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets";
|
|
34051
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
|
|
34052
|
+
}
|
|
34053
|
+
var MANIFEST_POLICY_FILE = "manifest-policy.json";
|
|
34054
|
+
function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
|
|
34055
|
+
try {
|
|
34056
|
+
deps.unlink(join15(runtimeConfigDir(home, identityId), "llm-source.json"));
|
|
34057
|
+
} catch (e) {
|
|
34058
|
+
if (e?.code !== "ENOENT") throw e;
|
|
34059
|
+
}
|
|
34060
|
+
}
|
|
34061
|
+
function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
|
|
34062
|
+
const parent3 = join15(home, ".securecoms", "runtime-config");
|
|
34063
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
34064
|
+
for (const d of [join15(home, ".securecoms"), parent3, dir]) {
|
|
34065
|
+
deps.mkdir(d, 448);
|
|
34066
|
+
deps.chmod(d, 448);
|
|
34067
|
+
}
|
|
34068
|
+
atomicWrite(join15(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
|
|
33566
34069
|
}
|
|
33567
34070
|
function atomicWrite(path, body, deps) {
|
|
33568
34071
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -33618,7 +34121,7 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
|
33618
34121
|
});
|
|
33619
34122
|
}
|
|
33620
34123
|
function lineHash(line) {
|
|
33621
|
-
return
|
|
34124
|
+
return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
|
|
33622
34125
|
}
|
|
33623
34126
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
33624
34127
|
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
@@ -33647,7 +34150,7 @@ var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
|
33647
34150
|
var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
|
|
33648
34151
|
var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
|
|
33649
34152
|
function legacySourceId(source, bytes) {
|
|
33650
|
-
return
|
|
34153
|
+
return createHash5("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
|
|
33651
34154
|
}
|
|
33652
34155
|
function localLegacySources(doc) {
|
|
33653
34156
|
if (!Array.isArray(doc.legacy_memory)) return [];
|
|
@@ -33819,6 +34322,568 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
33819
34322
|
return removed;
|
|
33820
34323
|
}
|
|
33821
34324
|
|
|
34325
|
+
// src/grants/binding.ts
|
|
34326
|
+
import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
34327
|
+
|
|
34328
|
+
// src/grants/manifest.ts
|
|
34329
|
+
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
34330
|
+
|
|
34331
|
+
// src/grants/pin.ts
|
|
34332
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
34333
|
+
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
34334
|
+
import { dirname as dirname4, join as join16 } from "node:path";
|
|
34335
|
+
var PIN_FILE_NAME = "owner-signing-key.json";
|
|
34336
|
+
var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
34337
|
+
var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
|
|
34338
|
+
function canonicalBase64Bytes(v, len) {
|
|
34339
|
+
if (typeof v !== "string" || v.length === 0) return null;
|
|
34340
|
+
const buf = Buffer.from(v, "base64");
|
|
34341
|
+
if (buf.length !== len || buf.toString("base64") !== v) return null;
|
|
34342
|
+
return new Uint8Array(buf);
|
|
34343
|
+
}
|
|
34344
|
+
function parsePinObject(v) {
|
|
34345
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return { ok: false, state: "malformed" };
|
|
34346
|
+
const keys = Object.keys(v).sort();
|
|
34347
|
+
if (keys.length !== PIN_KEYS.length || keys.some((k, i) => k !== PIN_KEYS[i])) return { ok: false, state: "malformed" };
|
|
34348
|
+
const o = v;
|
|
34349
|
+
if (typeof o.key_algo !== "string") return { ok: false, state: "malformed" };
|
|
34350
|
+
if (o.key_algo !== "ed25519") return { ok: false, state: "unsupported_algo" };
|
|
34351
|
+
if (typeof o.owner_identity_id !== "string" || !UUID_LOWER.test(o.owner_identity_id)) return { ok: false, state: "malformed" };
|
|
34352
|
+
const publicKey = canonicalBase64Bytes(o.public_key, 32);
|
|
34353
|
+
if (!publicKey) return { ok: false, state: "malformed" };
|
|
34354
|
+
try {
|
|
34355
|
+
if (ed255194.ExtendedPoint.fromHex(publicKey).isSmallOrder()) return { ok: false, state: "malformed" };
|
|
34356
|
+
} catch {
|
|
34357
|
+
return { ok: false, state: "malformed" };
|
|
34358
|
+
}
|
|
34359
|
+
return {
|
|
34360
|
+
ok: true,
|
|
34361
|
+
pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
|
|
34362
|
+
};
|
|
34363
|
+
}
|
|
34364
|
+
var NODE_FS = { lstat: (p) => lstatSync7(p), readFile: (p) => readFileSync9(p, "utf8") };
|
|
34365
|
+
function fromValue(v) {
|
|
34366
|
+
const r = parsePinObject(v);
|
|
34367
|
+
return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
|
|
34368
|
+
}
|
|
34369
|
+
function readPinFile(path, fs) {
|
|
34370
|
+
let st;
|
|
34371
|
+
try {
|
|
34372
|
+
st = fs.lstat(path);
|
|
34373
|
+
} catch (e) {
|
|
34374
|
+
if (e?.code === "ENOENT") return { kind: "absent" };
|
|
34375
|
+
return { kind: "bad", state: "unreadable" };
|
|
34376
|
+
}
|
|
34377
|
+
if (st.isSymbolicLink()) return { kind: "bad", state: "symlink_refused" };
|
|
34378
|
+
let text;
|
|
34379
|
+
try {
|
|
34380
|
+
text = fs.readFile(path);
|
|
34381
|
+
} catch {
|
|
34382
|
+
return { kind: "bad", state: "unreadable" };
|
|
34383
|
+
}
|
|
34384
|
+
if (st.mode & 63) writeServiceLog2("warn", "config.pin_permissions_unsafe");
|
|
34385
|
+
let json2;
|
|
34386
|
+
try {
|
|
34387
|
+
json2 = JSON.parse(text);
|
|
34388
|
+
} catch {
|
|
34389
|
+
return { kind: "bad", state: "malformed" };
|
|
34390
|
+
}
|
|
34391
|
+
return fromValue(json2);
|
|
34392
|
+
}
|
|
34393
|
+
function samePin(a, b) {
|
|
34394
|
+
return a.ownerIdentityId === b.ownerIdentityId && a.keyAlgo === b.keyAlgo && a.publicKeyB64 === b.publicKeyB64;
|
|
34395
|
+
}
|
|
34396
|
+
function loadOwnerGrantPin(opts) {
|
|
34397
|
+
const fs = opts.fs ?? NODE_FS;
|
|
34398
|
+
const file2 = readPinFile(join16(dirname4(opts.credentialPath), PIN_FILE_NAME), fs);
|
|
34399
|
+
const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
|
|
34400
|
+
if (file2.kind === "bad") return { state: file2.state, pin: null };
|
|
34401
|
+
if (field.kind === "bad") return { state: field.state, pin: null };
|
|
34402
|
+
if (file2.kind === "absent" && field.kind === "absent") return { state: "no_pin", pin: null };
|
|
34403
|
+
if (file2.kind === "ok" && field.kind === "ok") {
|
|
34404
|
+
return samePin(file2.pin, field.pin) ? { state: "ok", pin: file2.pin } : { state: "pin_conflict", pin: null };
|
|
34405
|
+
}
|
|
34406
|
+
const one = file2.kind === "ok" ? file2.pin : field.pin;
|
|
34407
|
+
return { state: "ok", pin: one };
|
|
34408
|
+
}
|
|
34409
|
+
|
|
34410
|
+
// src/grants/manifest.ts
|
|
34411
|
+
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
34412
|
+
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
34413
|
+
owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
|
|
34414
|
+
team_room: Object.freeze(["web_search"])
|
|
34415
|
+
});
|
|
34416
|
+
function manifestSignedBytes(manifest) {
|
|
34417
|
+
const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
|
|
34418
|
+
const body = canonicalize(manifest);
|
|
34419
|
+
const out = new Uint8Array(domain.length + body.length);
|
|
34420
|
+
out.set(domain, 0);
|
|
34421
|
+
out.set(body, domain.length);
|
|
34422
|
+
return out;
|
|
34423
|
+
}
|
|
34424
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34425
|
+
function wellTyped(v) {
|
|
34426
|
+
if (typeof v === "string") return true;
|
|
34427
|
+
if (typeof v === "number") return Number.isSafeInteger(v) && v >= 0;
|
|
34428
|
+
if (Array.isArray(v)) return v.every(wellTyped);
|
|
34429
|
+
if (isPlainObject(v)) return Object.values(v).every(wellTyped);
|
|
34430
|
+
return false;
|
|
34431
|
+
}
|
|
34432
|
+
var WRAPPER_KEYS = ["algo", "manifest", "signature"];
|
|
34433
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
34434
|
+
var PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
|
|
34435
|
+
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII.test(v);
|
|
34436
|
+
var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
|
|
34437
|
+
var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
34438
|
+
function parseEndpoints(v) {
|
|
34439
|
+
if (v === void 0) return {};
|
|
34440
|
+
if (!isPlainObject(v)) return null;
|
|
34441
|
+
const out = {};
|
|
34442
|
+
for (const [name, raw] of Object.entries(v)) {
|
|
34443
|
+
if (!isPlainObject(raw)) return null;
|
|
34444
|
+
const e = { ...raw };
|
|
34445
|
+
if (e.headersFile === "") delete e.headersFile;
|
|
34446
|
+
if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
|
|
34447
|
+
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII.test(e.baseUrl)) return null;
|
|
34448
|
+
let url;
|
|
34449
|
+
try {
|
|
34450
|
+
url = new URL(e.baseUrl);
|
|
34451
|
+
} catch {
|
|
34452
|
+
return null;
|
|
34453
|
+
}
|
|
34454
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
34455
|
+
if (url.username !== "" || url.password !== "" || url.href !== e.baseUrl) return null;
|
|
34456
|
+
if ("headersFile" in e && !absolutePath(e.headersFile)) return null;
|
|
34457
|
+
out[name] = Object.freeze({ baseUrl: e.baseUrl, ...typeof e.headersFile === "string" ? { headersFile: e.headersFile } : {} });
|
|
34458
|
+
}
|
|
34459
|
+
return out;
|
|
34460
|
+
}
|
|
34461
|
+
function parseDatabases(v) {
|
|
34462
|
+
if (v === void 0) return {};
|
|
34463
|
+
if (!isPlainObject(v)) return null;
|
|
34464
|
+
const out = {};
|
|
34465
|
+
for (const [name, d] of Object.entries(v)) {
|
|
34466
|
+
if (!isPlainObject(d)) return null;
|
|
34467
|
+
const keys = Object.keys(d);
|
|
34468
|
+
if (keys.length !== 1 || keys[0] !== "sqlite" && keys[0] !== "postgresUrlFile") return null;
|
|
34469
|
+
const path = d[keys[0]];
|
|
34470
|
+
if (!absolutePath(path)) return null;
|
|
34471
|
+
out[name] = Object.freeze(keys[0] === "sqlite" ? { sqlite: path } : { postgresUrlFile: path });
|
|
34472
|
+
}
|
|
34473
|
+
return out;
|
|
34474
|
+
}
|
|
34475
|
+
function parseConfig(v) {
|
|
34476
|
+
if (!isPlainObject(v)) return null;
|
|
34477
|
+
if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
|
|
34478
|
+
if (typeof v.persona_sha256 !== "string" || !HEX64.test(v.persona_sha256)) return null;
|
|
34479
|
+
if (typeof v.legacy_assign_sha256 !== "string" || !HEX64.test(v.legacy_assign_sha256)) return null;
|
|
34480
|
+
if (!isStringArray(v.flags)) return null;
|
|
34481
|
+
if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
|
|
34482
|
+
if ("llm" in v) {
|
|
34483
|
+
const l = v.llm;
|
|
34484
|
+
if (!isPlainObject(l) || !exactKeys(l, ["provider", "model"]) || typeof l.provider !== "string" || !l.provider || typeof l.model !== "string" || !l.model) return null;
|
|
34485
|
+
}
|
|
34486
|
+
if ("search" in v) {
|
|
34487
|
+
const sr = v.search;
|
|
34488
|
+
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
|
|
34489
|
+
}
|
|
34490
|
+
const mem = v.memory;
|
|
34491
|
+
if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
|
|
34492
|
+
if (mem.mode !== "markdown" && mem.mode !== "semantic" && mem.mode !== "off") return null;
|
|
34493
|
+
if (mem.mode === "semantic" !== "recall_days" in mem) return null;
|
|
34494
|
+
if ("recall_days" in mem && mem.recall_days !== 30 && mem.recall_days !== 90 && mem.recall_days !== 365) return null;
|
|
34495
|
+
return Object.freeze({
|
|
34496
|
+
persona_sha256: v.persona_sha256,
|
|
34497
|
+
...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
|
|
34498
|
+
...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
|
|
34499
|
+
...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
|
|
34500
|
+
flags: Object.freeze([...v.flags]),
|
|
34501
|
+
memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
|
|
34502
|
+
legacy_assign_sha256: v.legacy_assign_sha256
|
|
34503
|
+
});
|
|
34504
|
+
}
|
|
34505
|
+
function verifyToolGrantManifest(raw, ctx) {
|
|
34506
|
+
if (!isPlainObject(raw)) return { state: "malformed" };
|
|
34507
|
+
const keys = Object.keys(raw).sort();
|
|
34508
|
+
if (keys.length !== WRAPPER_KEYS.length || keys.some((k, i) => k !== WRAPPER_KEYS[i])) return { state: "malformed" };
|
|
34509
|
+
if (typeof raw.algo !== "string") return { state: "malformed" };
|
|
34510
|
+
if (raw.algo !== "ed25519") return { state: "unsupported_algo" };
|
|
34511
|
+
const m = raw.manifest;
|
|
34512
|
+
if (!isPlainObject(m)) return { state: "malformed" };
|
|
34513
|
+
if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
|
|
34514
|
+
if (!wellTyped(m)) return { state: "malformed" };
|
|
34515
|
+
if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
|
|
34516
|
+
for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
|
|
34517
|
+
if (k in m && typeof m[k] !== "string") return { state: "malformed" };
|
|
34518
|
+
}
|
|
34519
|
+
if (!isPlainObject(m.grants)) return { state: "malformed" };
|
|
34520
|
+
for (const list of Object.values(m.grants)) {
|
|
34521
|
+
if (!Array.isArray(list) || !list.every((t) => typeof t === "string")) return { state: "malformed" };
|
|
34522
|
+
}
|
|
34523
|
+
const endpoints = parseEndpoints(m.endpoints);
|
|
34524
|
+
const databases = parseDatabases(m.databases);
|
|
34525
|
+
const config = parseConfig(m.config);
|
|
34526
|
+
if (endpoints === null || databases === null || config === null) return { state: "malformed" };
|
|
34527
|
+
const sig = canonicalBase64Bytes(raw.signature, 64);
|
|
34528
|
+
if (!sig) return { state: "malformed" };
|
|
34529
|
+
let good;
|
|
34530
|
+
try {
|
|
34531
|
+
good = ed255195.verify(sig, manifestSignedBytes(m), ctx.pin.publicKey, { zip215: false });
|
|
34532
|
+
} catch {
|
|
34533
|
+
return { state: "malformed" };
|
|
34534
|
+
}
|
|
34535
|
+
if (!good) return { state: "bad_signature" };
|
|
34536
|
+
if (m.agent_identity_id !== ctx.agentIdentityId) return { state: "wrong_agent" };
|
|
34537
|
+
if (m.owner_identity_id !== ctx.pin.ownerIdentityId) return { state: "wrong_owner" };
|
|
34538
|
+
const counter = m.counter;
|
|
34539
|
+
if (typeof counter !== "number" || !Number.isSafeInteger(counter) || counter < 1) return { state: "malformed" };
|
|
34540
|
+
if (ctx.storedCounter !== null) {
|
|
34541
|
+
if (counter === ctx.storedCounter) return { state: "unchanged" };
|
|
34542
|
+
if (counter < ctx.storedCounter) return { state: "rollback" };
|
|
34543
|
+
}
|
|
34544
|
+
let droppedUnknown = false;
|
|
34545
|
+
const grants = {};
|
|
34546
|
+
for (const [audience, list] of Object.entries(m.grants)) {
|
|
34547
|
+
if (audience !== "owner_dm" && audience !== "team_room") {
|
|
34548
|
+
droppedUnknown = true;
|
|
34549
|
+
continue;
|
|
34550
|
+
}
|
|
34551
|
+
const known = MANIFEST_KNOWN_TOOLS[audience];
|
|
34552
|
+
const kept = list.filter((t) => known.includes(t));
|
|
34553
|
+
if (kept.length !== list.length) droppedUnknown = true;
|
|
34554
|
+
grants[audience] = Object.freeze(kept);
|
|
34555
|
+
}
|
|
34556
|
+
const manifest = Object.freeze({
|
|
34557
|
+
type: "securecoms.tool-grants",
|
|
34558
|
+
v: 1,
|
|
34559
|
+
agent_identity_id: m.agent_identity_id,
|
|
34560
|
+
owner_identity_id: m.owner_identity_id,
|
|
34561
|
+
...typeof m.owner_dm_channel_id === "string" ? { owner_dm_channel_id: m.owner_dm_channel_id } : {},
|
|
34562
|
+
...typeof m.owner_dm_relay_identity_id === "string" ? { owner_dm_relay_identity_id: m.owner_dm_relay_identity_id } : {},
|
|
34563
|
+
counter,
|
|
34564
|
+
grants: Object.freeze(grants),
|
|
34565
|
+
endpoints: Object.freeze(endpoints),
|
|
34566
|
+
databases: Object.freeze(databases),
|
|
34567
|
+
config
|
|
34568
|
+
});
|
|
34569
|
+
return { state: "ok", manifest, droppedUnknown, wrapper: raw };
|
|
34570
|
+
}
|
|
34571
|
+
|
|
34572
|
+
// src/grants/persona-render.ts
|
|
34573
|
+
var PERSONA_MAX_BYTES = 8192;
|
|
34574
|
+
var TONE_FIELD_ORDER = ["humor", "length", "warmth", "formality"];
|
|
34575
|
+
var TONE_WORDS = Object.freeze({
|
|
34576
|
+
warmth: { reserved: "Be plain and businesslike.", plain: "Be plain and businesslike.", warm: "Be warm.", friendly: "Be friendly." },
|
|
34577
|
+
length: { brief: "Keep answers short.", thorough: "Give the full picture when it matters." },
|
|
34578
|
+
formality: { casual: "Write casually.", formal: "Write formally." },
|
|
34579
|
+
humor: { none: "Do not use humor.", light: "Use light humor when it fits.", playful: "Be playful." }
|
|
34580
|
+
});
|
|
34581
|
+
var HANDOFF_TEXT = Object.freeze({
|
|
34582
|
+
money: "money is moving",
|
|
34583
|
+
legal: "someone wants a legal commitment",
|
|
34584
|
+
upset: "the person is upset"
|
|
34585
|
+
});
|
|
34586
|
+
var TOOL_PARAGRAPH = "Use a tool when the answer depends on something you cannot know. Which tools you have depends on the channel, so rely only on the tools offered in this turn, not on anything said earlier in a conversation. When a request needs a tool you were not offered, say so plainly and never claim you looked something up. When you searched or fetched a page, answer from what it says and name the source; never reply with only a link, and never read a URL aloud.";
|
|
34587
|
+
var PY_SPACE = /* @__PURE__ */ new Set([
|
|
34588
|
+
9,
|
|
34589
|
+
10,
|
|
34590
|
+
11,
|
|
34591
|
+
12,
|
|
34592
|
+
13,
|
|
34593
|
+
28,
|
|
34594
|
+
29,
|
|
34595
|
+
30,
|
|
34596
|
+
31,
|
|
34597
|
+
32,
|
|
34598
|
+
133,
|
|
34599
|
+
160,
|
|
34600
|
+
5760,
|
|
34601
|
+
8192,
|
|
34602
|
+
8193,
|
|
34603
|
+
8194,
|
|
34604
|
+
8195,
|
|
34605
|
+
8196,
|
|
34606
|
+
8197,
|
|
34607
|
+
8198,
|
|
34608
|
+
8199,
|
|
34609
|
+
8200,
|
|
34610
|
+
8201,
|
|
34611
|
+
8202,
|
|
34612
|
+
8232,
|
|
34613
|
+
8233,
|
|
34614
|
+
8239,
|
|
34615
|
+
8287,
|
|
34616
|
+
12288
|
|
34617
|
+
]);
|
|
34618
|
+
function pyStrip(s) {
|
|
34619
|
+
let a = 0;
|
|
34620
|
+
let b = s.length;
|
|
34621
|
+
while (a < b && PY_SPACE.has(s.charCodeAt(a))) a++;
|
|
34622
|
+
while (b > a && PY_SPACE.has(s.charCodeAt(b - 1))) b--;
|
|
34623
|
+
return s.slice(a, b);
|
|
34624
|
+
}
|
|
34625
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34626
|
+
var optString = (v) => v === void 0 || v === null || typeof v === "string";
|
|
34627
|
+
var optStrings = (v) => v === void 0 || v === null || Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
34628
|
+
function lines(items) {
|
|
34629
|
+
return (items ?? []).map(pyStrip).filter((x) => x !== "").map((x) => `- ${x}`).join("\n");
|
|
34630
|
+
}
|
|
34631
|
+
function renderPersonaFiles(persona) {
|
|
34632
|
+
if (!isObj(persona)) return { ok: false, reason: "shape" };
|
|
34633
|
+
const p = persona;
|
|
34634
|
+
for (const k of ["name", "business", "role", "manner"]) if (!optString(p[k])) return { ok: false, reason: "shape" };
|
|
34635
|
+
for (const k of ["can", "cannot", "handoff"]) if (!optStrings(p[k])) return { ok: false, reason: "shape" };
|
|
34636
|
+
const toneRaw = p.tone;
|
|
34637
|
+
if (!(toneRaw === void 0 || toneRaw === null || isObj(toneRaw) && Object.values(toneRaw).every((v) => typeof v === "string"))) {
|
|
34638
|
+
return { ok: false, reason: "shape" };
|
|
34639
|
+
}
|
|
34640
|
+
const name = pyStrip(p.name || "");
|
|
34641
|
+
const business = pyStrip(p.business || "");
|
|
34642
|
+
const role = pyStrip(p.role || "");
|
|
34643
|
+
const identity = [];
|
|
34644
|
+
if (name && business) identity.push(`You are ${name}, an agent for ${business}.`);
|
|
34645
|
+
else if (name) identity.push(`You are ${name}.`);
|
|
34646
|
+
if (role) identity.push(role);
|
|
34647
|
+
const identityMd = identity.join("\n\n");
|
|
34648
|
+
const tone = toneRaw ?? {};
|
|
34649
|
+
const soul = [];
|
|
34650
|
+
for (const k of TONE_FIELD_ORDER) {
|
|
34651
|
+
if (!Object.prototype.hasOwnProperty.call(tone, k)) continue;
|
|
34652
|
+
const words = TONE_WORDS[k];
|
|
34653
|
+
const v = tone[k];
|
|
34654
|
+
if (Object.prototype.hasOwnProperty.call(words, v)) soul.push(words[v]);
|
|
34655
|
+
}
|
|
34656
|
+
const manner = pyStrip(p.manner || "");
|
|
34657
|
+
let soulMd = soul.join(" ");
|
|
34658
|
+
if (manner) soulMd = pyStrip(`${soulMd}
|
|
34659
|
+
|
|
34660
|
+
${manner}`);
|
|
34661
|
+
const agents = [];
|
|
34662
|
+
const can = lines(p.can);
|
|
34663
|
+
if (can) agents.push(`What you can do:
|
|
34664
|
+
${can}`);
|
|
34665
|
+
const cannot = lines(p.cannot);
|
|
34666
|
+
if (cannot) agents.push(`What you cannot do:
|
|
34667
|
+
${cannot}`);
|
|
34668
|
+
const handoff = (p.handoff ?? []).filter((h) => pyStrip(h) !== "").map((h) => Object.prototype.hasOwnProperty.call(HANDOFF_TEXT, h) ? HANDOFF_TEXT[h] : pyStrip(h));
|
|
34669
|
+
if (handoff.length > 0) agents.push("Hand off to a person when " + handoff.join("; when ") + ". Say plainly that you are doing so.");
|
|
34670
|
+
agents.push(TOOL_PARAGRAPH);
|
|
34671
|
+
const agentsMd = agents.join("\n\n");
|
|
34672
|
+
const files = { "IDENTITY.md": identityMd, "SOUL.md": soulMd, "AGENTS.md": agentsMd };
|
|
34673
|
+
for (const body of Object.values(files)) {
|
|
34674
|
+
if (Buffer.byteLength(body, "utf8") > PERSONA_MAX_BYTES) return { ok: false, reason: "over_cap" };
|
|
34675
|
+
}
|
|
34676
|
+
return { ok: true, files };
|
|
34677
|
+
}
|
|
34678
|
+
|
|
34679
|
+
// src/grants/binding.ts
|
|
34680
|
+
var CONFIG_BINDING_FIELDS = ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"];
|
|
34681
|
+
var ASSIGN_PREFIX = "legacy-fact-assign:";
|
|
34682
|
+
function legacyAssignSha256(prunes) {
|
|
34683
|
+
const set = [...new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)))].sort();
|
|
34684
|
+
return sha256Hex(canonicalize(set));
|
|
34685
|
+
}
|
|
34686
|
+
function legacyAssignToken(d) {
|
|
34687
|
+
return `${ASSIGN_PREFIX}${d.source_id}:${d.fact_hash}:${d.channel_id}`;
|
|
34688
|
+
}
|
|
34689
|
+
var PERSONA_FILES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
|
|
34690
|
+
var LLM_SOURCE = "llm-source.json";
|
|
34691
|
+
var FAIL = Symbol("bind_fail");
|
|
34692
|
+
function guarded(f) {
|
|
34693
|
+
try {
|
|
34694
|
+
return f();
|
|
34695
|
+
} catch {
|
|
34696
|
+
return FAIL;
|
|
34697
|
+
}
|
|
34698
|
+
}
|
|
34699
|
+
var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34700
|
+
var strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? [...v] : void 0;
|
|
34701
|
+
var sameStrings = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((x, i) => x === b[i]);
|
|
34702
|
+
var sha256Hex = (bytes) => Buffer.from(sha25611(bytes)).toString("hex");
|
|
34703
|
+
function endpointEquals(raw, signed) {
|
|
34704
|
+
if (!isObj2(raw)) return false;
|
|
34705
|
+
const e = { ...raw };
|
|
34706
|
+
if (e.headersFile === "") delete e.headersFile;
|
|
34707
|
+
const keys = Object.keys(e).sort().join(",");
|
|
34708
|
+
const want = "headersFile" in signed ? "baseUrl,headersFile" : "baseUrl";
|
|
34709
|
+
return keys === want && e.baseUrl === signed.baseUrl && e.headersFile === signed.headersFile;
|
|
34710
|
+
}
|
|
34711
|
+
function databaseEquals(raw, signed) {
|
|
34712
|
+
if (!isObj2(raw)) return false;
|
|
34713
|
+
const keys = Object.keys(raw);
|
|
34714
|
+
if (keys.length !== 1) return false;
|
|
34715
|
+
return "sqlite" in signed ? raw.sqlite === signed.sqlite : raw.postgresUrlFile === signed.postgresUrlFile;
|
|
34716
|
+
}
|
|
34717
|
+
function bindTargets(raw, signed, equals) {
|
|
34718
|
+
if (raw === void 0) return { written: {}, mismatches: 0 };
|
|
34719
|
+
if (!isObj2(raw)) return { written: {}, mismatches: 1 };
|
|
34720
|
+
const written = {};
|
|
34721
|
+
let mismatches = 0;
|
|
34722
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
34723
|
+
const s = Object.prototype.hasOwnProperty.call(signed, name) ? signed[name] : void 0;
|
|
34724
|
+
if (s !== void 0 && equals(entry, s)) written[name] = s;
|
|
34725
|
+
else mismatches++;
|
|
34726
|
+
}
|
|
34727
|
+
return { written, mismatches };
|
|
34728
|
+
}
|
|
34729
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
|
|
34730
|
+
const mismatches = /* @__PURE__ */ new Set();
|
|
34731
|
+
const cfg = manifest.config;
|
|
34732
|
+
const out = {};
|
|
34733
|
+
const files = isObj2(payload.files) ? payload.files : {};
|
|
34734
|
+
const personaOk = guarded(() => sha256Hex(canonicalize(payload.persona)) === cfg.persona_sha256);
|
|
34735
|
+
const render = guarded(() => renderPersonaFiles(payload.persona));
|
|
34736
|
+
if (personaOk === true && render !== FAIL && render.ok) {
|
|
34737
|
+
for (const f of PERSONA_FILES) out[f] = render.files[f];
|
|
34738
|
+
} else {
|
|
34739
|
+
mismatches.add("persona");
|
|
34740
|
+
}
|
|
34741
|
+
const delivered = files[LLM_SOURCE];
|
|
34742
|
+
const llmSame = guarded(() => {
|
|
34743
|
+
if (delivered === void 0) return cfg.llm === void 0;
|
|
34744
|
+
if (typeof delivered !== "string") return false;
|
|
34745
|
+
const parsed = JSON.parse(delivered);
|
|
34746
|
+
if (!isObj2(parsed)) return false;
|
|
34747
|
+
if (cfg.llm === void 0) return parsed.provider === void 0 && parsed.model === void 0;
|
|
34748
|
+
return parsed.provider === cfg.llm.provider && parsed.model === cfg.llm.model;
|
|
34749
|
+
});
|
|
34750
|
+
if (llmSame !== true) mismatches.add("llm");
|
|
34751
|
+
if (cfg.llm !== void 0) {
|
|
34752
|
+
out[LLM_SOURCE] = JSON.stringify({ provider: cfg.llm.provider, model: cfg.llm.model, version: payload.version }) + "\n";
|
|
34753
|
+
}
|
|
34754
|
+
if (!isObj2(payload.files) && payload.files !== void 0) mismatches.add("files");
|
|
34755
|
+
for (const name of Object.keys(files)) {
|
|
34756
|
+
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
34757
|
+
}
|
|
34758
|
+
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
34759
|
+
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
34760
|
+
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
34761
|
+
const payloadFlags = strings(sj?.flags);
|
|
34762
|
+
if (!sameStrings(payloadFlags, cfg.flags)) mismatches.add("flags");
|
|
34763
|
+
const tools = isObj2(sj?.tools) ? sj.tools : void 0;
|
|
34764
|
+
if (!sameStrings(strings(tools?.allow), cfg.tools_allow)) mismatches.add("tools_allow");
|
|
34765
|
+
const deny = strings(tools?.deny);
|
|
34766
|
+
const ps = isObj2(sj?.search) ? sj.search : void 0;
|
|
34767
|
+
const payloadProvider = ps?.provider;
|
|
34768
|
+
let searchBlock;
|
|
34769
|
+
let appliedSearch;
|
|
34770
|
+
if (payloadProvider === cfg.search?.provider && ps === void 0 === (cfg.search === void 0)) {
|
|
34771
|
+
searchBlock = ps === void 0 ? null : Object.freeze({ provider: ps.provider, ...typeof ps.keyFile === "string" ? { keyFile: ps.keyFile } : {} });
|
|
34772
|
+
appliedSearch = searchBlock;
|
|
34773
|
+
} else {
|
|
34774
|
+
mismatches.add("search");
|
|
34775
|
+
searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
|
|
34776
|
+
}
|
|
34777
|
+
const skillsDoc = {
|
|
34778
|
+
endpoints: eps.written,
|
|
34779
|
+
databases: dbs.written,
|
|
34780
|
+
flags: [...cfg.flags],
|
|
34781
|
+
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
34782
|
+
...searchBlock ? { search: searchBlock } : {}
|
|
34783
|
+
};
|
|
34784
|
+
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
34785
|
+
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
34786
|
+
let memory;
|
|
34787
|
+
if (isObj2(payload.memory)) {
|
|
34788
|
+
const pm = payload.memory;
|
|
34789
|
+
const sameMode = pm.mode === cfg.memory.mode;
|
|
34790
|
+
const sameDays = cfg.memory.mode === "semantic" ? pm.recall_days === cfg.memory.recall_days : !("recall_days" in pm);
|
|
34791
|
+
if (!sameMode || !sameDays) mismatches.add("memory");
|
|
34792
|
+
const { recall_days: _r, mode: _m, ...rest } = pm;
|
|
34793
|
+
memory = { ...signedMemory, ...rest };
|
|
34794
|
+
} else {
|
|
34795
|
+
mismatches.add("memory");
|
|
34796
|
+
memory = { ...signedMemory };
|
|
34797
|
+
}
|
|
34798
|
+
const prunes = payload.memory_prunes === void 0 ? [] : payload.memory_prunes;
|
|
34799
|
+
const decisions = Array.isArray(payload.legacy_memory_fact_decisions) ? payload.legacy_memory_fact_decisions : [];
|
|
34800
|
+
const assignMatch = guarded(() => Array.isArray(prunes) && legacyAssignSha256(prunes) === cfg.legacy_assign_sha256);
|
|
34801
|
+
let legacyDecisions;
|
|
34802
|
+
if (assignMatch === true) {
|
|
34803
|
+
const verified = new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)));
|
|
34804
|
+
legacyDecisions = decisions.filter((d) => {
|
|
34805
|
+
if (!isObj2(d) || d.action !== "assign") return true;
|
|
34806
|
+
if (typeof d.source_id !== "string" || typeof d.fact_hash !== "string" || typeof d.channel_id !== "string") return false;
|
|
34807
|
+
return verified.has(legacyAssignToken({ source_id: d.source_id, fact_hash: d.fact_hash, channel_id: d.channel_id }));
|
|
34808
|
+
});
|
|
34809
|
+
} else {
|
|
34810
|
+
mismatches.add("legacy_assign");
|
|
34811
|
+
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
34812
|
+
}
|
|
34813
|
+
const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
|
|
34814
|
+
const policy = {
|
|
34815
|
+
v: 1,
|
|
34816
|
+
manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
|
|
34817
|
+
secret_path_mode: "enforce"
|
|
34818
|
+
};
|
|
34819
|
+
return {
|
|
34820
|
+
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
34821
|
+
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
34822
|
+
files: out,
|
|
34823
|
+
memory,
|
|
34824
|
+
signedTargets: { endpoints: Object.keys(eps.written).length, databases: Object.keys(dbs.written).length },
|
|
34825
|
+
policy,
|
|
34826
|
+
appliedSearch,
|
|
34827
|
+
legacyDecisions
|
|
34828
|
+
};
|
|
34829
|
+
}
|
|
34830
|
+
|
|
34831
|
+
// src/grants/room-search-cap.ts
|
|
34832
|
+
var ROOM_SEARCH_DAILY_CAP = 50;
|
|
34833
|
+
var ROOM_SEARCH_CAP_ENV = "SECURECOMS_ROOM_SEARCH_DAILY_CAP";
|
|
34834
|
+
function parseRoomSearchCap(v, warn) {
|
|
34835
|
+
if (v === void 0) return ROOM_SEARCH_DAILY_CAP;
|
|
34836
|
+
if (/^\d+$/.test(v)) {
|
|
34837
|
+
const n = Number(v);
|
|
34838
|
+
if (n <= ROOM_SEARCH_DAILY_CAP) return n;
|
|
34839
|
+
}
|
|
34840
|
+
warn?.();
|
|
34841
|
+
return ROOM_SEARCH_DAILY_CAP;
|
|
34842
|
+
}
|
|
34843
|
+
var AGENT_ROOM_SEARCH_DAILY_CAP = 200;
|
|
34844
|
+
var AGENT_ROOM_SEARCH_CAP_ENV = "SECURECOMS_AGENT_ROOM_SEARCH_DAILY_CAP";
|
|
34845
|
+
function parseAgentRoomSearchCap(v, warn) {
|
|
34846
|
+
if (v === void 0) return AGENT_ROOM_SEARCH_DAILY_CAP;
|
|
34847
|
+
if (/^\d+$/.test(v)) {
|
|
34848
|
+
const n = Number(v);
|
|
34849
|
+
if (n <= AGENT_ROOM_SEARCH_DAILY_CAP) return n;
|
|
34850
|
+
}
|
|
34851
|
+
warn?.();
|
|
34852
|
+
return AGENT_ROOM_SEARCH_DAILY_CAP;
|
|
34853
|
+
}
|
|
34854
|
+
var AGENT_TOTAL_KEY = "agent-total";
|
|
34855
|
+
|
|
34856
|
+
// src/grants/effective.ts
|
|
34857
|
+
var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
|
|
34858
|
+
var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
|
|
34859
|
+
function effectiveToolGrants(i) {
|
|
34860
|
+
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
|
|
34861
|
+
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
|
|
34862
|
+
const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
|
|
34863
|
+
const listed = new Set(i.manifest.grants[i.audience] ?? []);
|
|
34864
|
+
const signed = ceiling.filter((t) => listed.has(t));
|
|
34865
|
+
if (!i.eligible || !i.manifestSwitchOn) {
|
|
34866
|
+
const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
|
|
34867
|
+
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
|
|
34868
|
+
}
|
|
34869
|
+
let withheld = false;
|
|
34870
|
+
const tools = signed.filter((t) => {
|
|
34871
|
+
if (i.disabled.has(t)) return false;
|
|
34872
|
+
if (t === "http_request" && i.signedTargets.endpoints === 0) return false;
|
|
34873
|
+
if (t === "sql_query" && i.signedTargets.databases === 0) return false;
|
|
34874
|
+
if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
|
|
34875
|
+
withheld = true;
|
|
34876
|
+
return false;
|
|
34877
|
+
}
|
|
34878
|
+
return true;
|
|
34879
|
+
});
|
|
34880
|
+
return {
|
|
34881
|
+
grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
|
|
34882
|
+
source: "manifest",
|
|
34883
|
+
withheldBySearchCap: withheld
|
|
34884
|
+
};
|
|
34885
|
+
}
|
|
34886
|
+
|
|
33822
34887
|
// src/bridge-agent.ts
|
|
33823
34888
|
import { homedir as homedir4 } from "node:os";
|
|
33824
34889
|
|
|
@@ -33907,6 +34972,50 @@ function roomIsCrossOrg(rows) {
|
|
|
33907
34972
|
return unknown ? null : false;
|
|
33908
34973
|
}
|
|
33909
34974
|
|
|
34975
|
+
// src/grants/owner-turn.ts
|
|
34976
|
+
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
34977
|
+
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
34978
|
+
function verifyOwnerSignature(entry, pin) {
|
|
34979
|
+
if (!entry.identitySignature || !entry.payloadHex) return { state: "no_signature" };
|
|
34980
|
+
if (!HEX.test(entry.payloadHex)) return { state: "malformed" };
|
|
34981
|
+
const sig = canonicalBase64Bytes(entry.identitySignature, 64);
|
|
34982
|
+
if (!sig) return { state: "malformed" };
|
|
34983
|
+
const bytes = new Uint8Array(Buffer.from(entry.payloadHex, "hex"));
|
|
34984
|
+
let envelope;
|
|
34985
|
+
try {
|
|
34986
|
+
envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
34987
|
+
} catch {
|
|
34988
|
+
return { state: "malformed" };
|
|
34989
|
+
}
|
|
34990
|
+
const algo = envelope?.identity_proof?.algo;
|
|
34991
|
+
if (typeof envelope !== "object" || envelope === null || algo === void 0 || algo === null) return { state: "malformed" };
|
|
34992
|
+
if (algo !== "ed25519") return { state: "unknown_algo" };
|
|
34993
|
+
let good;
|
|
34994
|
+
try {
|
|
34995
|
+
good = ed255196.verify(sig, bytes, pin.publicKey, { zip215: false });
|
|
34996
|
+
} catch {
|
|
34997
|
+
return { state: "malformed" };
|
|
34998
|
+
}
|
|
34999
|
+
return good ? { state: "ok", envelope } : { state: "bad_signature" };
|
|
35000
|
+
}
|
|
35001
|
+
function classifyOwnerDmLeaves(leaves, allowed) {
|
|
35002
|
+
if (leaves.some((l) => !l.basic || l.identity === null)) return "foreign";
|
|
35003
|
+
if (!leaves.some((l) => l.identity === allowed.ownerIdentityId)) return "foreign";
|
|
35004
|
+
const unexpected = /* @__PURE__ */ new Set();
|
|
35005
|
+
for (const l of leaves) {
|
|
35006
|
+
const id = l.identity;
|
|
35007
|
+
if (id === allowed.ownerIdentityId || id === allowed.agentIdentityId) continue;
|
|
35008
|
+
if (allowed.relayIdentityId !== void 0 && id === allowed.relayIdentityId) continue;
|
|
35009
|
+
unexpected.add(id);
|
|
35010
|
+
}
|
|
35011
|
+
if (unexpected.size === 0) return "ok";
|
|
35012
|
+
if (unexpected.size === 1) {
|
|
35013
|
+
const [only] = unexpected;
|
|
35014
|
+
if (allowed.isServerRelay(only)) return "unsigned_relay";
|
|
35015
|
+
}
|
|
35016
|
+
return "foreign";
|
|
35017
|
+
}
|
|
35018
|
+
|
|
33910
35019
|
// src/bridge-agent.ts
|
|
33911
35020
|
function logSendSupersededAdoption(kind2) {
|
|
33912
35021
|
switch (kind2) {
|
|
@@ -33959,6 +35068,9 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
33959
35068
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
33960
35069
|
]);
|
|
33961
35070
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35071
|
+
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35072
|
+
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
35073
|
+
var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
|
|
33962
35074
|
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
33963
35075
|
const v = env.SECURECOMS_OWNER_DM_DEFAULT_GRANTS;
|
|
33964
35076
|
return v === void 0 || v === "on";
|
|
@@ -33967,6 +35079,14 @@ function roomDefaultGrantsEnabled(env = process.env) {
|
|
|
33967
35079
|
const v = env.SECURECOMS_ROOM_DEFAULT_GRANTS;
|
|
33968
35080
|
return v === void 0 || v === "on";
|
|
33969
35081
|
}
|
|
35082
|
+
function ownerDmManifestGrantsEnabled(env = process.env) {
|
|
35083
|
+
const v = env.SECURECOMS_OWNER_DM_MANIFEST_GRANTS;
|
|
35084
|
+
return v === void 0 || v === "on";
|
|
35085
|
+
}
|
|
35086
|
+
function roomManifestGrantsEnabled(env = process.env) {
|
|
35087
|
+
const v = env.SECURECOMS_ROOM_MANIFEST_GRANTS;
|
|
35088
|
+
return v === void 0 || v === "on";
|
|
35089
|
+
}
|
|
33970
35090
|
var MAX_PENDING_NOTE_REQUESTS = 8;
|
|
33971
35091
|
var INGEST_STAMP_KEY = "_sc_notes_ingest_generation";
|
|
33972
35092
|
function validIngestStamp(v) {
|
|
@@ -34053,6 +35173,17 @@ function redeliveryDelayMs(attempts) {
|
|
|
34053
35173
|
return 2 ** Math.min(Math.max(attempts, 0), 16) * REDELIVERY_BACKOFF_BASE_MS;
|
|
34054
35174
|
}
|
|
34055
35175
|
var DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
|
|
35176
|
+
function holdsManifestGrant(a) {
|
|
35177
|
+
return a?.tool_grants?.some((g) => g.mode === "read" && g.source === "manifest") ?? false;
|
|
35178
|
+
}
|
|
35179
|
+
function greetingAuthorization(a) {
|
|
35180
|
+
const { memory_write: _w, ...rest } = a;
|
|
35181
|
+
return Object.freeze({ ...rest, tool_grants: NO_TOOL_GRANTS });
|
|
35182
|
+
}
|
|
35183
|
+
function utcDayOf(ms) {
|
|
35184
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
35185
|
+
}
|
|
35186
|
+
var ROOM_SEARCH_CAP_NOTICE = "I've used today's web searches for this room. I can still help without searching. Searches are available again tomorrow (UTC).";
|
|
34056
35187
|
var DEAD_LETTER_NOTICE_INTERVAL_MS = 60 * 6e4;
|
|
34057
35188
|
var PAUSE_RECHECK_MS = 3e4;
|
|
34058
35189
|
var SUPERSEDE_RECHECK_MS = 3e4;
|
|
@@ -34181,7 +35312,7 @@ var FileOwnerAuthorityStore = class {
|
|
|
34181
35312
|
async save(enclaveId, pin) {
|
|
34182
35313
|
const all = await this.readAll();
|
|
34183
35314
|
all[enclaveId] = pin;
|
|
34184
|
-
await fsp.mkdir(
|
|
35315
|
+
await fsp.mkdir(dirname5(this.path), { recursive: true });
|
|
34185
35316
|
await fsp.writeFile(this.path, JSON.stringify(all), "utf8");
|
|
34186
35317
|
}
|
|
34187
35318
|
};
|
|
@@ -34212,6 +35343,12 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
34212
35343
|
var RoomGrantEpochMovedError = class extends Error {
|
|
34213
35344
|
};
|
|
34214
35345
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
35346
|
+
var SessionSupersededError = class extends Error {
|
|
35347
|
+
constructor() {
|
|
35348
|
+
super("session_superseded: another copy of this agent took its session during start()");
|
|
35349
|
+
this.name = "SessionSupersededError";
|
|
35350
|
+
}
|
|
35351
|
+
};
|
|
34215
35352
|
var GroupSupersededSendError = class extends Error {
|
|
34216
35353
|
constructor(channelId, activeGroupId) {
|
|
34217
35354
|
super(`send refused for channel ${channelId}: the channel moved to a new secure group; not sent`);
|
|
@@ -34251,9 +35388,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34251
35388
|
const seed = hexToBytes4(cfg.signingPrivateKey);
|
|
34252
35389
|
this.custody = SoftwareKeyCustody.fromPrivateKey("ed25519", seed);
|
|
34253
35390
|
this.replyIdKey = deriveReplyIdKey(seed);
|
|
35391
|
+
const base = deps.fetchImpl ?? fetch;
|
|
35392
|
+
this.fetchImpl = (input, init) => this.sessionSuperseded ? Promise.reject(new Error("session_superseded")) : base(input, init);
|
|
34254
35393
|
this.transport = deps.transport ?? new RestTransport({
|
|
34255
35394
|
baseUrl: cfg.apiBase,
|
|
34256
35395
|
bearerToken: cfg.apiKey,
|
|
35396
|
+
fetchImpl: this.fetchImpl,
|
|
35397
|
+
wsImpl: deps.wsImpl,
|
|
35398
|
+
// A 4409 close means another copy of this agent took the session.
|
|
35399
|
+
// Every other terminal close keeps today's behaviour.
|
|
35400
|
+
onTerminalClose: (close) => {
|
|
35401
|
+
if (close.code === WS_CLOSE_SESSION_SUPERSEDED2) this.onSessionSuperseded();
|
|
35402
|
+
},
|
|
34257
35403
|
connectIdentityId: cfg.identityId,
|
|
34258
35404
|
// P1b: hardened delivery-ping reconnect (verify-before-terminal +
|
|
34259
35405
|
// half-open + wake + auth war-cap). On by default for the daemon;
|
|
@@ -34267,7 +35413,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34267
35413
|
else writeServiceLog2("warn", "transport.delivery_ping_lifecycle_unknown");
|
|
34268
35414
|
}
|
|
34269
35415
|
});
|
|
34270
|
-
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey });
|
|
35416
|
+
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey, fetchImpl: this.fetchImpl });
|
|
34271
35417
|
this.store = new SqliteMlsStateStore({
|
|
34272
35418
|
dbPath: cfg.storePath,
|
|
34273
35419
|
identityId: cfg.identityId,
|
|
@@ -34338,8 +35484,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34338
35484
|
// throws (mirrors the divergence-heal signal path).
|
|
34339
35485
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
34340
35486
|
});
|
|
34341
|
-
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
34342
35487
|
this.home = deps.home ?? homedir4();
|
|
35488
|
+
this.grantWriters = {
|
|
35489
|
+
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
35490
|
+
writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
|
|
35491
|
+
bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
|
|
35492
|
+
removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
|
|
35493
|
+
};
|
|
34343
35494
|
this.platform = deps.platform ?? process.platform;
|
|
34344
35495
|
this.machinePaths = deps.hostMachine === true || deps.home !== void 0;
|
|
34345
35496
|
this.unifiedOn = unifiedPart1On(deps.unifiedMemoryPart1);
|
|
@@ -34354,7 +35505,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34354
35505
|
threshold: EMBEDDING_MANIFEST.similarityThreshold,
|
|
34355
35506
|
warm: windowsSemanticOn(this.platform)
|
|
34356
35507
|
};
|
|
34357
|
-
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init,
|
|
35508
|
+
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname5(fileURLToPath2(import.meta.url)));
|
|
34358
35509
|
}
|
|
34359
35510
|
});
|
|
34360
35511
|
this.embedder = deps.embedder ?? (deps.hostMachine === true && deps.home === void 0 ? new EmbedderClient({ identityId: cfg.identityId, home: homedir4(), source: cfg.modelSource, fetchImpl: this.fetchImpl, platform: this.platform }) : INERT_EMBEDDER);
|
|
@@ -34365,7 +35516,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34365
35516
|
modelId: EMBEDDING_MANIFEST.modelId,
|
|
34366
35517
|
threshold: EMBEDDING_MANIFEST.similarityThreshold
|
|
34367
35518
|
});
|
|
34368
|
-
this.heartbeat = new CallHeartbeat(this.machinePaths ?
|
|
35519
|
+
this.heartbeat = new CallHeartbeat(this.machinePaths ? join17(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
|
|
34369
35520
|
this.reembed = new ReembedQueue({
|
|
34370
35521
|
store: this.store,
|
|
34371
35522
|
embedder: this.embedder,
|
|
@@ -34447,8 +35598,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34447
35598
|
const j = await res.json();
|
|
34448
35599
|
return String(j.attachment_id);
|
|
34449
35600
|
};
|
|
34450
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
34451
|
-
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(
|
|
35601
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join17(dirname5(cfg.storePath), "attachments");
|
|
35602
|
+
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join17(dirname5(cfg.storePath), "vault-owner-pins.json"));
|
|
34452
35603
|
this.action = {
|
|
34453
35604
|
begin: (channelId) => this.actionBegin(channelId),
|
|
34454
35605
|
start: (opts) => this.actionStart(opts),
|
|
@@ -34464,8 +35615,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34464
35615
|
// stored for peek-then-join in ingestOnce
|
|
34465
35616
|
custody;
|
|
34466
35617
|
fetchImpl;
|
|
35618
|
+
/** P-1: set once a 4409 close says another copy took this agent's session.
|
|
35619
|
+
* The gated fetch built in the constructor refuses every request after it. */
|
|
35620
|
+
sessionSuperseded = false;
|
|
35621
|
+
/** C9: who hears `session_superseded` (runMode's quiesce). */
|
|
35622
|
+
sessionSupersededListeners = /* @__PURE__ */ new Set();
|
|
35623
|
+
/** C9 (spec §4.1.6, low 3): the one close() run every caller shares. */
|
|
35624
|
+
closing = null;
|
|
34467
35625
|
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
34468
35626
|
home;
|
|
35627
|
+
grantWriters;
|
|
35628
|
+
/** Owner grants (spec §7.5, OQ-2): the operator cap, read once; the same variable the runtime reads. 0 = no room search. */
|
|
35629
|
+
roomSearchDailyCap = parseRoomSearchCap(process.env[ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("cap_invalid", ["cap_invalid"]) }));
|
|
35630
|
+
/** Review M-3 (ERIC APPROVALS 3): the per-agent total across rooms, read once; the same variable the runtime reads. */
|
|
35631
|
+
agentRoomSearchDailyCap = parseAgentRoomSearchCap(process.env[AGENT_ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("agent_cap_invalid", ["agent_cap_invalid"]) }));
|
|
35632
|
+
/** `${day}\n${channel}` keys already logged reached today (in memory; the store holds the durable flag). */
|
|
35633
|
+
roomCapLogged = /* @__PURE__ */ new Set();
|
|
35634
|
+
/** Rooms whose K-14 send is in flight: a second frame posts nothing more. */
|
|
35635
|
+
roomCapNoticePending = /* @__PURE__ */ new Set();
|
|
35636
|
+
/** Held entries whose staged copy lost web_search to the backstop; applied again at release. */
|
|
35637
|
+
backstopStripped = /* @__PURE__ */ new Set();
|
|
35638
|
+
/**
|
|
35639
|
+
* Owner grants plan 4 Task 11 (P-4, P-6): what the managed directory holds under the current manifest.
|
|
35640
|
+
* Manifest grants require state "bound" at the applied counter (Task 13). null at startup, while a
|
|
35641
|
+
* refetch narrows it, and after any throw (the fallback).
|
|
35642
|
+
*/
|
|
35643
|
+
manifestBinding = null;
|
|
35644
|
+
/** skills.json tools.deny from the latest fetch, manifest or not (spec §5.2 `disabled`). */
|
|
35645
|
+
payloadToolDeny = /* @__PURE__ */ new Set();
|
|
35646
|
+
/** Spec §7.7 (OQ-5): the latest fetch's binding outcome, reported in tool_grant_status (Task 15). */
|
|
35647
|
+
configBindingMismatches = [];
|
|
35648
|
+
targetMismatches = { endpoint: 0, database: 0 };
|
|
35649
|
+
/** P-5 (Task 14): the legacy fact decisions the latest bound fetch may apply (assigns verified against the signed set). */
|
|
35650
|
+
boundLegacyDecisions = [];
|
|
34469
35651
|
joinState = /* @__PURE__ */ new Map();
|
|
34470
35652
|
/**
|
|
34471
35653
|
* Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
|
|
@@ -34769,11 +35951,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34769
35951
|
ownerDmDefaultGrants = ownerDmDefaultGrantsEnabled();
|
|
34770
35952
|
/** Read once at startup; see roomDefaultGrantsEnabled. */
|
|
34771
35953
|
roomDefaultGrants = roomDefaultGrantsEnabled();
|
|
35954
|
+
/** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
|
|
35955
|
+
ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
|
|
35956
|
+
roomManifestGrants = roomManifestGrantsEnabled();
|
|
34772
35957
|
/**
|
|
34773
35958
|
* Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
|
|
34774
35959
|
* cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
|
|
34775
35960
|
*/
|
|
34776
35961
|
ownerDmCycleFresh = /* @__PURE__ */ new Set();
|
|
35962
|
+
// Owner-signed tool grants, step 1 (plan 1 task 7). The verified manifest is
|
|
35963
|
+
// RECORDED here and in the sealed store; no grant code reads it in step 1.
|
|
35964
|
+
toolGrantManifest = null;
|
|
35965
|
+
toolGrantCounter = null;
|
|
35966
|
+
manifestState = "no_manifest";
|
|
35967
|
+
/** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
|
|
35968
|
+
ownerVerifyFailures = 0;
|
|
35969
|
+
/** Verified owner turns of this ingest cycle, recorded in owner_replay when staged (P-3). */
|
|
35970
|
+
ownerReplayPending = /* @__PURE__ */ new Map();
|
|
35971
|
+
/** D-9: ids of owner turns that failed verification (never `replay`, audit E-3), id -> channel. Persisted too. */
|
|
35972
|
+
ownerVerifyFailedIds = /* @__PURE__ */ new Map();
|
|
35973
|
+
serverOwnerMismatchLogged = false;
|
|
35974
|
+
/** Audit L-2: the replay prune runs at most once per interval, on a monotonic clock. */
|
|
35975
|
+
monoNow = () => performance.now();
|
|
35976
|
+
lastOwnerReplayPruneAt = -Infinity;
|
|
34777
35977
|
/**
|
|
34778
35978
|
* Audit M-B1: room-granted turns, keyed by the inbound message id, with the
|
|
34779
35979
|
* channel, MLS group and epoch the room was classified at (review I-1: an
|
|
@@ -34897,6 +36097,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34897
36097
|
action;
|
|
34898
36098
|
waiters = [];
|
|
34899
36099
|
pingHandle;
|
|
36100
|
+
/** C10 (P-15): the delivery-ping subscription start() opens at its top,
|
|
36101
|
+
* held unconsumed until startReceivePump() takes it. */
|
|
36102
|
+
earlyPings = null;
|
|
36103
|
+
/** C10 fix round 1: settles (never rejects) when start()'s body does; close()
|
|
36104
|
+
* awaits it, bounded, so the store never closes under a start() step. */
|
|
36105
|
+
startInFlight = null;
|
|
34900
36106
|
safetyTimer;
|
|
34901
36107
|
/** The ingest cycle now running, if any (see ingestOnce: single-flight). */
|
|
34902
36108
|
ingestInFlight = null;
|
|
@@ -35216,14 +36422,38 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35216
36422
|
return;
|
|
35217
36423
|
}
|
|
35218
36424
|
const version = payload.version;
|
|
35219
|
-
|
|
36425
|
+
const skillsForDeny = payload.skills_json;
|
|
36426
|
+
const denyRaw = skillsForDeny?.tools?.deny;
|
|
36427
|
+
this.payloadToolDeny = new Set(Array.isArray(denyRaw) ? denyRaw.filter((t) => typeof t === "string") : []);
|
|
36428
|
+
let manifestStepThrew = false;
|
|
36429
|
+
try {
|
|
36430
|
+
await this.applyToolGrantManifest(payload.tool_grant_manifest);
|
|
36431
|
+
} catch (e) {
|
|
36432
|
+
manifestStepThrew = true;
|
|
36433
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
36434
|
+
}
|
|
36435
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
36436
|
+
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
36437
|
+
if (inForce !== null && pin !== null) {
|
|
36438
|
+
const applied = manifestStepThrew ? await this.bindFailed(inForce, payload.memory, void 0) : await this.applyUnderManifest(inForce, pin.publicKeyB64, payload, version);
|
|
36439
|
+
if (!applied) {
|
|
36440
|
+
await this.postRuntimeConfigReport(base, version, payload.skills_json ?? {});
|
|
36441
|
+
return;
|
|
36442
|
+
}
|
|
36443
|
+
} else {
|
|
36444
|
+
this.manifestBinding = null;
|
|
36445
|
+
this.configBindingMismatches = [];
|
|
36446
|
+
this.targetMismatches = { endpoint: 0, database: 0 };
|
|
36447
|
+
await this.applyPayloadMemory(payload.memory);
|
|
36448
|
+
this.overwriteStalePolicy();
|
|
36449
|
+
}
|
|
35220
36450
|
if (!version) {
|
|
35221
36451
|
writeManagedFiles(this.home, this.cfg.identityId, {});
|
|
35222
36452
|
writeServiceLog2("info", "runtime_config.not_saved");
|
|
35223
36453
|
}
|
|
35224
36454
|
const skillsJson = payload.skills_json ?? {};
|
|
35225
|
-
if (version) {
|
|
35226
|
-
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
36455
|
+
if (version && inForce === null) {
|
|
36456
|
+
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
|
|
35227
36457
|
...payload.files ?? {},
|
|
35228
36458
|
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
35229
36459
|
`
|
|
@@ -35235,12 +36465,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35235
36465
|
if (payload.legacy_memory_deletes?.length) {
|
|
35236
36466
|
deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
|
|
35237
36467
|
}
|
|
35238
|
-
|
|
36468
|
+
const legacyDecisions = inForce !== null ? this.boundLegacyDecisions : payload.legacy_memory_fact_decisions;
|
|
36469
|
+
if (legacyDecisions?.length) {
|
|
35239
36470
|
const provider = this.cfg.memoryProvider ?? "securecoms";
|
|
35240
36471
|
const result = await applyLegacyMemoryFactDecisions(
|
|
35241
36472
|
this.home,
|
|
35242
36473
|
this.cfg.identityId,
|
|
35243
|
-
|
|
36474
|
+
legacyDecisions,
|
|
35244
36475
|
async (channelId, fact, assignmentKey) => {
|
|
35245
36476
|
if (provider === "none" || provider === "hermes") {
|
|
35246
36477
|
throw new Error("configured memory provider has no protected channel-fact import path");
|
|
@@ -35259,72 +36490,177 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35259
36490
|
});
|
|
35260
36491
|
}
|
|
35261
36492
|
}
|
|
35262
|
-
|
|
35263
|
-
const report = {
|
|
35264
|
-
applied_version: version,
|
|
35265
|
-
paths: checkConfiguredPaths(skillsJson),
|
|
35266
|
-
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
35267
|
-
// the file as it now stands, not the one they just pruned from.
|
|
35268
|
-
memory: readManagedMemory(this.home, this.cfg.identityId),
|
|
35269
|
-
// E13: pass through what the runtime reported. The daemon does not
|
|
35270
|
-
// interpret any of it.
|
|
35271
|
-
local_persona: local?.persona ?? null,
|
|
35272
|
-
// Only the runtime knows whether a search key file is configured — it
|
|
35273
|
-
// is an environment value on that process, invisible to the daemon and
|
|
35274
|
-
// to the backend. The backend needs it to generate a truthful tool
|
|
35275
|
-
// paragraph, so it rides along here.
|
|
35276
|
-
has_search_key: local?.has_search_key ?? false,
|
|
35277
|
-
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
35278
|
-
// whether one is configured in its own environment.
|
|
35279
|
-
has_llm_key: local?.has_llm_key ?? false,
|
|
35280
|
-
// The backend uses this only to expose target channels compatible with
|
|
35281
|
-
// the actual protected-memory adapter. It is an enum, never a path or
|
|
35282
|
-
// provider credential.
|
|
35283
|
-
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
35284
|
-
// Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
|
|
35285
|
-
// unchanged. memory_notes is true only when notes are actually on (the
|
|
35286
|
-
// provider is securecoms and the parsed payload.memory keeps them on);
|
|
35287
|
-
// the generation is the store's applied value, never the payload's.
|
|
35288
|
-
// Final review I-1: and only once a notes-reading runtime said hello.
|
|
35289
|
-
memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
|
|
35290
|
-
// Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
|
|
35291
|
-
// 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
|
|
35292
|
-
// booleans only (U1).
|
|
35293
|
-
...this.reportedUnified(),
|
|
35294
|
-
...this.reportedClearGeneration(),
|
|
35295
|
-
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
35296
|
-
...this.reportedRecall(),
|
|
35297
|
-
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
35298
|
-
// regular-file bytes and strips every local path before this leaves the
|
|
35299
|
-
// machine.
|
|
35300
|
-
legacy_memory: local?.legacy_memory ?? [],
|
|
35301
|
-
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
35302
|
-
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
35303
|
-
// one enum only; readLocalReport rebuilds it so no path rides along.
|
|
35304
|
-
secret_paths: local?.secret_paths ?? null
|
|
35305
|
-
};
|
|
35306
|
-
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
35307
|
-
method: "POST",
|
|
35308
|
-
headers: {
|
|
35309
|
-
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
35310
|
-
"content-type": "application/json"
|
|
35311
|
-
},
|
|
35312
|
-
body: JSON.stringify(report)
|
|
35313
|
-
});
|
|
35314
|
-
if (!posted.ok) {
|
|
35315
|
-
if (version) {
|
|
35316
|
-
writeServiceLog2("warn", "runtime_config.report_failed", { status: posted.status, version });
|
|
35317
|
-
} else {
|
|
35318
|
-
writeServiceLog2("warn", "runtime_config.legacy_memory_report_failed", { status: posted.status });
|
|
35319
|
-
}
|
|
35320
|
-
return;
|
|
35321
|
-
}
|
|
35322
|
-
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
35323
|
-
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
36493
|
+
await this.postRuntimeConfigReport(base, version, skillsJson);
|
|
35324
36494
|
} catch (e) {
|
|
35325
36495
|
writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
|
|
35326
36496
|
}
|
|
35327
36497
|
}
|
|
36498
|
+
/**
|
|
36499
|
+
* Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
|
|
36500
|
+
* after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
|
|
36501
|
+
* the binding left null.
|
|
36502
|
+
*/
|
|
36503
|
+
async applyUnderManifest(manifest, pinKeyB64, payload, version) {
|
|
36504
|
+
let prior;
|
|
36505
|
+
try {
|
|
36506
|
+
prior = (await this.store.loadToolGrantApplied(pinKeyB64))?.search;
|
|
36507
|
+
} catch (e) {
|
|
36508
|
+
prior = void 0;
|
|
36509
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_read_failed", ["applied_read_failed"]) }, e);
|
|
36510
|
+
}
|
|
36511
|
+
let bound;
|
|
36512
|
+
try {
|
|
36513
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
|
|
36514
|
+
} catch (e) {
|
|
36515
|
+
bound = null;
|
|
36516
|
+
return this.bindFailed(manifest, payload.memory, e);
|
|
36517
|
+
}
|
|
36518
|
+
const counter = manifest.counter;
|
|
36519
|
+
const state = bound.mismatches.length === 0 ? "bound" : "mismatch";
|
|
36520
|
+
const old = this.manifestBinding;
|
|
36521
|
+
if (!(old !== null && old.state === "bound" && old.counter === counter && state === "bound")) this.manifestBinding = null;
|
|
36522
|
+
this.configBindingMismatches = bound.mismatches;
|
|
36523
|
+
this.boundLegacyDecisions = bound.legacyDecisions;
|
|
36524
|
+
this.logConfigBinding(bound);
|
|
36525
|
+
this.targetMismatches = { endpoint: bound.targetMismatches.endpoint, database: bound.targetMismatches.database };
|
|
36526
|
+
await this.applyPayloadMemory(bound.memory);
|
|
36527
|
+
if (!version) {
|
|
36528
|
+
this.manifestBinding = null;
|
|
36529
|
+
return true;
|
|
36530
|
+
}
|
|
36531
|
+
try {
|
|
36532
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
|
|
36533
|
+
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
36534
|
+
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
36535
|
+
} catch (e) {
|
|
36536
|
+
this.manifestBinding = null;
|
|
36537
|
+
throw e;
|
|
36538
|
+
}
|
|
36539
|
+
this.manifestBinding = { counter, state, signedTargets: { ...bound.signedTargets } };
|
|
36540
|
+
if (bound.appliedSearch !== void 0) {
|
|
36541
|
+
try {
|
|
36542
|
+
await this.store.saveToolGrantApplied(pinKeyB64, { counter, search: bound.appliedSearch });
|
|
36543
|
+
} catch (e) {
|
|
36544
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_save_failed", ["applied_save_failed"]) }, e);
|
|
36545
|
+
}
|
|
36546
|
+
}
|
|
36547
|
+
return true;
|
|
36548
|
+
}
|
|
36549
|
+
/** Spec §7.6 (plan 4 Task 15): content-free. One line per mismatched field, one per dropped target. */
|
|
36550
|
+
logConfigBinding(bound) {
|
|
36551
|
+
if (bound.mismatches.length === 0) {
|
|
36552
|
+
writeServiceLog2("info", "authz.config_binding", { state: ServiceLogEnum.of("ok", ["ok"]) });
|
|
36553
|
+
}
|
|
36554
|
+
for (const field of bound.mismatches) {
|
|
36555
|
+
writeServiceLog2("warn", "authz.config_binding", {
|
|
36556
|
+
state: ServiceLogEnum.of("mismatch", ["mismatch"]),
|
|
36557
|
+
field: ServiceLogEnum.of(field, ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"])
|
|
36558
|
+
});
|
|
36559
|
+
}
|
|
36560
|
+
for (const kind2 of ["endpoint", "database"]) {
|
|
36561
|
+
for (let i = 0; i < bound.targetMismatches[kind2]; i++) {
|
|
36562
|
+
writeServiceLog2("warn", "authz.target_mismatch", { kind: ServiceLogEnum.of(kind2, ["endpoint", "database"]) });
|
|
36563
|
+
}
|
|
36564
|
+
}
|
|
36565
|
+
}
|
|
36566
|
+
/** H-4: the binding could not be computed. The memory still applies, with the signed mode; nothing else is written. */
|
|
36567
|
+
async bindFailed(manifest, rawMemory, e) {
|
|
36568
|
+
this.manifestBinding = null;
|
|
36569
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
36570
|
+
const signed = { mode: manifest.config.memory.mode, ...manifest.config.memory.recall_days !== void 0 ? { recall_days: manifest.config.memory.recall_days } : {} };
|
|
36571
|
+
let memory = signed;
|
|
36572
|
+
if (rawMemory !== null && typeof rawMemory === "object" && !Array.isArray(rawMemory)) {
|
|
36573
|
+
const { mode: _m, recall_days: _r, ...rest } = rawMemory;
|
|
36574
|
+
memory = { ...signed, ...rest };
|
|
36575
|
+
}
|
|
36576
|
+
await this.applyPayloadMemory(memory);
|
|
36577
|
+
return false;
|
|
36578
|
+
}
|
|
36579
|
+
/** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
|
|
36580
|
+
overwriteStalePolicy() {
|
|
36581
|
+
if (!existsSync5(join17(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
|
|
36582
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
|
|
36583
|
+
}
|
|
36584
|
+
/** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
|
|
36585
|
+
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
36586
|
+
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
36587
|
+
const report = {
|
|
36588
|
+
applied_version: version,
|
|
36589
|
+
paths: checkConfiguredPaths(skillsJson),
|
|
36590
|
+
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
36591
|
+
// the file as it now stands, not the one they just pruned from.
|
|
36592
|
+
memory: readManagedMemory(this.home, this.cfg.identityId),
|
|
36593
|
+
// E13: pass through what the runtime reported. The daemon does not
|
|
36594
|
+
// interpret any of it.
|
|
36595
|
+
local_persona: local?.persona ?? null,
|
|
36596
|
+
// Only the runtime knows whether a search key file is configured — it
|
|
36597
|
+
// is an environment value on that process, invisible to the daemon and
|
|
36598
|
+
// to the backend. The backend needs it to generate a truthful tool
|
|
36599
|
+
// paragraph, so it rides along here.
|
|
36600
|
+
has_search_key: local?.has_search_key ?? false,
|
|
36601
|
+
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
36602
|
+
// whether one is configured in its own environment.
|
|
36603
|
+
has_llm_key: local?.has_llm_key ?? false,
|
|
36604
|
+
// The backend uses this only to expose target channels compatible with
|
|
36605
|
+
// the actual protected-memory adapter. It is an enum, never a path or
|
|
36606
|
+
// provider credential.
|
|
36607
|
+
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
36608
|
+
// Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
|
|
36609
|
+
// unchanged. memory_notes is true only when notes are actually on (the
|
|
36610
|
+
// provider is securecoms and the parsed payload.memory keeps them on);
|
|
36611
|
+
// the generation is the store's applied value, never the payload's.
|
|
36612
|
+
// Final review I-1: and only once a notes-reading runtime said hello.
|
|
36613
|
+
memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
|
|
36614
|
+
// Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
|
|
36615
|
+
// 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
|
|
36616
|
+
// booleans only (U1).
|
|
36617
|
+
...this.reportedUnified(),
|
|
36618
|
+
...this.reportedClearGeneration(),
|
|
36619
|
+
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
36620
|
+
...this.reportedRecall(),
|
|
36621
|
+
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
36622
|
+
// regular-file bytes and strips every local path before this leaves the
|
|
36623
|
+
// machine.
|
|
36624
|
+
legacy_memory: local?.legacy_memory ?? [],
|
|
36625
|
+
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
36626
|
+
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
36627
|
+
// one enum only; readLocalReport rebuilds it so no path rides along.
|
|
36628
|
+
secret_paths: local?.secret_paths ?? null,
|
|
36629
|
+
// Owner-signed tool grants (spec §7.7, D-6): enums and numbers only. It
|
|
36630
|
+
// authorizes nothing and a compromised backend can forge what it shows
|
|
36631
|
+
// (residual 9). The backend drops it until Plan 2 or 3 stores it.
|
|
36632
|
+
tool_grant_status: {
|
|
36633
|
+
pin: this.cfg.ownerGrantPin?.state ?? "no_pin",
|
|
36634
|
+
manifest: this.manifestState,
|
|
36635
|
+
applied_counter: this.toolGrantCounter,
|
|
36636
|
+
owner_dm_verify_failures: this.ownerVerifyFailures,
|
|
36637
|
+
// Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
|
|
36638
|
+
config_binding_mismatches: [...this.configBindingMismatches],
|
|
36639
|
+
target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
|
|
36640
|
+
capability: this.lastConsumerCapability
|
|
36641
|
+
}
|
|
36642
|
+
};
|
|
36643
|
+
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
36644
|
+
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
36645
|
+
method: "POST",
|
|
36646
|
+
headers: {
|
|
36647
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
36648
|
+
"content-type": "application/json"
|
|
36649
|
+
},
|
|
36650
|
+
body: JSON.stringify(report)
|
|
36651
|
+
});
|
|
36652
|
+
if (!posted.ok) {
|
|
36653
|
+
if (version) {
|
|
36654
|
+
writeServiceLog2("warn", "runtime_config.report_failed", { status: posted.status, version });
|
|
36655
|
+
} else {
|
|
36656
|
+
writeServiceLog2("warn", "runtime_config.legacy_memory_report_failed", { status: posted.status });
|
|
36657
|
+
}
|
|
36658
|
+
return;
|
|
36659
|
+
}
|
|
36660
|
+
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
36661
|
+
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
36662
|
+
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
36663
|
+
}
|
|
35328
36664
|
/**
|
|
35329
36665
|
* Enforce the inbound-buffer cap with a DROP-OLDEST policy.
|
|
35330
36666
|
*
|
|
@@ -35344,9 +36680,58 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35344
36680
|
this.droppedMessages += overflow;
|
|
35345
36681
|
}
|
|
35346
36682
|
}
|
|
36683
|
+
/**
|
|
36684
|
+
* C10 (spec P-15, OQ-8 (b)): subscribe early, consume late. The
|
|
36685
|
+
* delivery-ping socket opens here, before anything else, so this copy claims
|
|
36686
|
+
* the device's session (and another copy gets its 4409) BEFORE this copy's
|
|
36687
|
+
* boot drain runs. The pings it carries are NOT consumed until
|
|
36688
|
+
* startReceivePump(), the same place as before: starting the pump first is
|
|
36689
|
+
* still forbidden (DRAIN BEFORE ROTATE, startBody).
|
|
36690
|
+
*
|
|
36691
|
+
* A 4409 during start() (audit H-3): the gated fetch is already set (C9 step
|
|
36692
|
+
* 0). start() then arms nothing more (supersededDuringStart before each
|
|
36693
|
+
* timer and before the pump), runs the memoized close() quiesce, and rejects
|
|
36694
|
+
* with SessionSupersededError. A step that FAILED because of the supersede
|
|
36695
|
+
* (a refused fetch, a store the quiesce closed) is the same outcome.
|
|
36696
|
+
*/
|
|
35347
36697
|
async start() {
|
|
36698
|
+
const early = this.transport.subscribeDeliveryPings("");
|
|
36699
|
+
this.earlyPings = early;
|
|
36700
|
+
this.pingHandle = { close: early.close };
|
|
36701
|
+
const body = this.startBody();
|
|
36702
|
+
this.startInFlight = body.then(() => {
|
|
36703
|
+
}, () => {
|
|
36704
|
+
});
|
|
36705
|
+
try {
|
|
36706
|
+
await body;
|
|
36707
|
+
} catch (e) {
|
|
36708
|
+
if (!this.sessionSuperseded) {
|
|
36709
|
+
if (this.earlyPings) {
|
|
36710
|
+
this.earlyPings.close();
|
|
36711
|
+
this.earlyPings = null;
|
|
36712
|
+
this.pingHandle = void 0;
|
|
36713
|
+
}
|
|
36714
|
+
throw e;
|
|
36715
|
+
}
|
|
36716
|
+
if (!(e instanceof SessionSupersededError)) writeServiceLog2("warn", "startup.superseded_start_error", {}, e);
|
|
36717
|
+
await this.close().catch(() => {
|
|
36718
|
+
});
|
|
36719
|
+
throw new SessionSupersededError();
|
|
36720
|
+
}
|
|
36721
|
+
}
|
|
36722
|
+
/** C10: true once a 4409 landed; checked before each arming step of start(). */
|
|
36723
|
+
supersededDuringStart() {
|
|
36724
|
+
return this.sessionSuperseded;
|
|
36725
|
+
}
|
|
36726
|
+
async startBody() {
|
|
35348
36727
|
this.startedAt = Date.now();
|
|
35349
36728
|
await this.restoreRuntimeInbox();
|
|
36729
|
+
await this.loadToolGrantState();
|
|
36730
|
+
try {
|
|
36731
|
+
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
36732
|
+
} catch (e) {
|
|
36733
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
36734
|
+
}
|
|
35350
36735
|
try {
|
|
35351
36736
|
this.store.pruneRecall();
|
|
35352
36737
|
this.store.reconcileRecallClearGeneration();
|
|
@@ -35398,10 +36783,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35398
36783
|
await this.warmRestoredNotes();
|
|
35399
36784
|
await this.reindexRestoredTurns();
|
|
35400
36785
|
if (this.unifiedOn) await this.reindexUnifiedRestored();
|
|
36786
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
35401
36787
|
this.runtimeConfigTimer = setInterval(() => {
|
|
35402
36788
|
void this.refetchRuntimeConfig();
|
|
35403
36789
|
}, 6 * 60 * 60 * 1e3);
|
|
35404
36790
|
this.runtimeConfigTimer.unref?.();
|
|
36791
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
35405
36792
|
this.membershipSweepTimer = setInterval(() => {
|
|
35406
36793
|
if (this.closingDown || this.membershipSweepWork) return;
|
|
35407
36794
|
this.membershipSweepWork = this.runMembershipSweeps().finally(() => {
|
|
@@ -35409,6 +36796,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35409
36796
|
});
|
|
35410
36797
|
}, PURGE_SWEEP_INTERVAL_MS);
|
|
35411
36798
|
this.membershipSweepTimer.unref?.();
|
|
36799
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
35412
36800
|
this.recallPruneTimer = setInterval(() => {
|
|
35413
36801
|
if (this.closingDown || this.recallPruneWork) return;
|
|
35414
36802
|
this.recallPruneWork = this.runRecallPruneTick().finally(() => {
|
|
@@ -35416,6 +36804,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35416
36804
|
});
|
|
35417
36805
|
}, 60 * 60 * 1e3);
|
|
35418
36806
|
this.recallPruneTimer.unref?.();
|
|
36807
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
35419
36808
|
if (this.machinePaths) this.recallMaintenanceTimer = setInterval(() => {
|
|
35420
36809
|
if (this.closingDown) return;
|
|
35421
36810
|
const n = ++this.maintenanceTickN;
|
|
@@ -35426,6 +36815,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35426
36815
|
});
|
|
35427
36816
|
}, 1e3);
|
|
35428
36817
|
this.recallMaintenanceTimer?.unref?.();
|
|
36818
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
35429
36819
|
this.startReceivePump();
|
|
35430
36820
|
if (drainedInTime) {
|
|
35431
36821
|
this.armSelfHealSweeps();
|
|
@@ -36480,8 +37870,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36480
37870
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
36481
37871
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
36482
37872
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
36483
|
-
if (
|
|
36484
|
-
|
|
37873
|
+
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
37874
|
+
const origin = this.spokenReplyIds.has(id) ? "fast_path" : !this.taintScope(channelId) ? "reply" : opts.inReplyTo !== void 0 && this.cleanTurns.has(opts.inReplyTo) && !this.taintedTurns.has(opts.inReplyTo) ? "reply" : "untrusted";
|
|
37875
|
+
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
37876
|
+
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
36485
37877
|
}
|
|
36486
37878
|
/**
|
|
36487
37879
|
* Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
|
|
@@ -36496,8 +37888,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36496
37888
|
return { ...this.store.itemCapture(channelId), clearGeneration: stamp };
|
|
36497
37889
|
}
|
|
36498
37890
|
/** Section 7.1 agent reply, section 24.2: the owner-DM predicate for the channel at write (no sender), else nothing. Reference trust always (U18). OQ-P2: bridge-authored Pinned:/Forgot: lines are filtered by the caller. */
|
|
36499
|
-
spawnUnifiedReply(channelId, text, messageId, expect) {
|
|
37891
|
+
spawnUnifiedReply(channelId, text, messageId, expect, origin) {
|
|
36500
37892
|
if (expect === null) return;
|
|
37893
|
+
if (origin === "fast_path") return;
|
|
36501
37894
|
const work = (async () => {
|
|
36502
37895
|
try {
|
|
36503
37896
|
if (!this.unifiedProviderOn()) return;
|
|
@@ -36513,7 +37906,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36513
37906
|
source: "agent_reply",
|
|
36514
37907
|
trust: "reference",
|
|
36515
37908
|
text,
|
|
36516
|
-
expect
|
|
37909
|
+
expect,
|
|
37910
|
+
role: "agent",
|
|
37911
|
+
origin
|
|
36517
37912
|
});
|
|
36518
37913
|
} catch {
|
|
36519
37914
|
writeServiceLog2("warn", "memory.unified_reply_failed", {});
|
|
@@ -37239,6 +38634,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37239
38634
|
);
|
|
37240
38635
|
return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
|
|
37241
38636
|
}
|
|
38637
|
+
/**
|
|
38638
|
+
* Decision D-9 (audit H-3): a turn that failed owner verification gets only
|
|
38639
|
+
* its own text, never the prior-turn history snapshot, for every provider.
|
|
38640
|
+
*/
|
|
38641
|
+
mayReadHistoryForTurn(messageId, authorization) {
|
|
38642
|
+
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38643
|
+
}
|
|
37242
38644
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
37243
38645
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
37244
38646
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -37248,16 +38650,119 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37248
38650
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
37249
38651
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
37250
38652
|
*/
|
|
37251
|
-
async memoryForTurn(channelId, authorization, limit = 50) {
|
|
37252
|
-
if (!this.
|
|
38653
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
38654
|
+
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
37253
38655
|
return { recent: [], facts: [] };
|
|
37254
38656
|
}
|
|
37255
38657
|
try {
|
|
37256
|
-
return await this.memory.context(channelId, limit);
|
|
38658
|
+
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
37257
38659
|
} catch {
|
|
37258
38660
|
return { recent: [], facts: [] };
|
|
37259
38661
|
}
|
|
37260
38662
|
}
|
|
38663
|
+
/**
|
|
38664
|
+
* Owner grants plan 4 Task 16 (T-30, audit r2 H-1): the voice fence is on whenever a pin is loaded, on EVERY runtime
|
|
38665
|
+
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
38666
|
+
*/
|
|
38667
|
+
voiceFenceOn() {
|
|
38668
|
+
return this.cfg.ownerGrantPin?.pin != null;
|
|
38669
|
+
}
|
|
38670
|
+
/** The keyed and legacy fast-reply ids of every peer row of the conversation (audit r2 M-1; never the 50-row window). */
|
|
38671
|
+
fenceContext(channelId) {
|
|
38672
|
+
let ids;
|
|
38673
|
+
const build = () => {
|
|
38674
|
+
const out = /* @__PURE__ */ new Set();
|
|
38675
|
+
for (const p of this.store.memoryPeerMessageIds(channelId)) {
|
|
38676
|
+
out.add(fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, p));
|
|
38677
|
+
out.add(legacyFastReplyId(p));
|
|
38678
|
+
}
|
|
38679
|
+
return out;
|
|
38680
|
+
};
|
|
38681
|
+
return { get fastIds() {
|
|
38682
|
+
return ids ??= build();
|
|
38683
|
+
} };
|
|
38684
|
+
}
|
|
38685
|
+
/**
|
|
38686
|
+
* Drop agent lines classified fast_path, untrusted or unknown from a turn's memory (every turn of a pinned agent).
|
|
38687
|
+
* `strict` (review H-3, a turn that holds a manifest grant): relay-class peer lines go too (sender not the pinned
|
|
38688
|
+
* owner, or voice modality). Otherwise peer lines are never dropped.
|
|
38689
|
+
*/
|
|
38690
|
+
fenceMemory(channelId, ctx, manifestTurn = false) {
|
|
38691
|
+
if (!this.voiceFenceOn()) return ctx;
|
|
38692
|
+
const scoped = this.taintScope(channelId);
|
|
38693
|
+
const strict = manifestTurn && scoped;
|
|
38694
|
+
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38695
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38696
|
+
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38697
|
+
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38698
|
+
const fence = this.fenceContext(channelId);
|
|
38699
|
+
const recent = ctx.recent.filter((t) => {
|
|
38700
|
+
const m = meta.get(t.seq);
|
|
38701
|
+
if (t.role !== "agent") return !(strict && isRelayClassPeer({ peerId: t.peerId, modality: m?.modality }, ownerId));
|
|
38702
|
+
const origin = classifyAgentLine({ ...m ?? {}, ts: t.ts }, fence);
|
|
38703
|
+
return origin === "reply" || origin === "untrusted" && !scoped;
|
|
38704
|
+
});
|
|
38705
|
+
return recent.length === ctx.recent.length ? ctx : { ...ctx, recent };
|
|
38706
|
+
}
|
|
38707
|
+
/**
|
|
38708
|
+
* Review H-3: true when a delivered frame holds a line a manifest turn would exclude (a relay-class peer line; agent
|
|
38709
|
+
* lines in a frame are already fenced). Such a turn's reply is recorded "untrusted".
|
|
38710
|
+
*/
|
|
38711
|
+
frameHoldsRelayClass(m) {
|
|
38712
|
+
const recent = m.memory?.recent ?? [];
|
|
38713
|
+
if (recent.length === 0) return false;
|
|
38714
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38715
|
+
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38716
|
+
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38717
|
+
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
38718
|
+
}
|
|
38719
|
+
/**
|
|
38720
|
+
* Review H-3 attribution: message ids of turns delivered on a pinned agent whose frame held no relay-class line.
|
|
38721
|
+
* A reply is recorded "reply" only when its in-reply-to is here; anything else (tainted, unknown after a restart,
|
|
38722
|
+
* no in-reply-to) is "untrusted". Bounded; the oldest are forgotten first (forgetting only makes a reply untrusted).
|
|
38723
|
+
*/
|
|
38724
|
+
cleanTurns = /* @__PURE__ */ new Set();
|
|
38725
|
+
noteDeliveredFrame(m, unifiedTainted = false) {
|
|
38726
|
+
if (!this.taintScope(m.channel_id)) return;
|
|
38727
|
+
const tainted = unifiedTainted || this.turnIsRelayClass(m) || !holdsManifestGrant(m.authorization) && this.frameHoldsRelayClass(m);
|
|
38728
|
+
if (tainted) {
|
|
38729
|
+
this.cleanTurns.delete(m.message_id);
|
|
38730
|
+
this.taintedTurns.add(m.message_id);
|
|
38731
|
+
if (this.taintedTurns.size > 5e3) this.taintedTurns.delete(this.taintedTurns.values().next().value);
|
|
38732
|
+
return;
|
|
38733
|
+
}
|
|
38734
|
+
if (this.taintedTurns.has(m.message_id)) return;
|
|
38735
|
+
this.cleanTurns.add(m.message_id);
|
|
38736
|
+
if (this.cleanTurns.size > 5e3) this.cleanTurns.delete(this.cleanTurns.values().next().value);
|
|
38737
|
+
}
|
|
38738
|
+
taintedTurns = /* @__PURE__ */ new Set();
|
|
38739
|
+
/**
|
|
38740
|
+
* Review R2-H2: on a pinned agent a 7a note or a unified memory_note is written only from a turn delivered with a clean
|
|
38741
|
+
* frame in the owner DM (cleanTurns, not taintedTurns), so a tainted fallback turn cannot plant a note that a later
|
|
38742
|
+
* manifest turn reads. Taint is noted at delivery, before any note frame for the turn can arrive.
|
|
38743
|
+
*/
|
|
38744
|
+
noteTurnClean(messageId) {
|
|
38745
|
+
return !this.voiceFenceOn() || this.cleanTurns.has(messageId) && !this.taintedTurns.has(messageId);
|
|
38746
|
+
}
|
|
38747
|
+
/**
|
|
38748
|
+
* Review R2-H1: the turn itself is relay-class (its frame memory is prior-turn only, so its own sender is never in it):
|
|
38749
|
+
* a sender that is not the pinned owner, a relay speaker claim, or a voice turn.
|
|
38750
|
+
*/
|
|
38751
|
+
turnIsRelayClass(m) {
|
|
38752
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
|
|
38753
|
+
return m.sender_id !== ownerId || m.authorization?.relay_speaker !== void 0 || m.voice === true || m.transcribed === true || m.speaker !== void 0;
|
|
38754
|
+
}
|
|
38755
|
+
/**
|
|
38756
|
+
* R2-M1 ruling: the channel where the H-3 taint and the `untrusted` exclusion apply, on a pinned agent: the signed
|
|
38757
|
+
* owner_dm_channel_id of the manifest in force. Before any manifest is held there is no signed DM, so every channel
|
|
38758
|
+
* not known to be a room counts (fail closed: a reply recorded there before the first manifest must not reach it).
|
|
38759
|
+
*/
|
|
38760
|
+
taintScope(channelId) {
|
|
38761
|
+
if (!this.voiceFenceOn()) return false;
|
|
38762
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
38763
|
+
if (signedDm !== void 0) return signedDm === channelId;
|
|
38764
|
+
return (this.freshMeta(channelId)?.is_dm ?? this.store.getChannelMeta(channelId)?.isDm) !== false;
|
|
38765
|
+
}
|
|
37261
38766
|
/**
|
|
37262
38767
|
* Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
|
|
37263
38768
|
* value turns notes off and deletes nothing. A readable one goes through
|
|
@@ -37713,10 +39218,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37713
39218
|
if (isDm !== true || ownerId === void 0) return { kind: "unknown" };
|
|
37714
39219
|
return this.store.recordDmOwner(c, ownerId) ? { kind: "owner", ownerId } : { kind: "unknown" };
|
|
37715
39220
|
};
|
|
39221
|
+
const fences = /* @__PURE__ */ new Map();
|
|
39222
|
+
const classifyHistoryOrigin = (c, r) => {
|
|
39223
|
+
const f = fences.get(c) ?? (fences.set(c, this.fenceContext(c)), fences.get(c));
|
|
39224
|
+
return classifyAgentLine({ origin: r.origin, modality: r.modality, ...r.messageId ? { messageId: r.messageId } : {}, ts: r.recordedAt }, f);
|
|
39225
|
+
};
|
|
37716
39226
|
const out = await runMigration({
|
|
37717
39227
|
store: this.store,
|
|
37718
39228
|
items: this.items,
|
|
37719
39229
|
classify,
|
|
39230
|
+
classifyHistoryOrigin,
|
|
37720
39231
|
stopping: () => this.closingDown,
|
|
37721
39232
|
pinsChanged: () => this.notes.exclusive(async () => {
|
|
37722
39233
|
this.unifiedReads?.bumpGeneration();
|
|
@@ -37736,7 +39247,24 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37736
39247
|
const a = m.authorization;
|
|
37737
39248
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
37738
39249
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
37739
|
-
|
|
39250
|
+
if (scopeId === null) return void 0;
|
|
39251
|
+
if (!this.voiceFenceOn()) return this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId);
|
|
39252
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
39253
|
+
const ctxFor = (c) => contexts.get(c) ?? (contexts.set(c, this.fenceContext(c)), contexts.get(c));
|
|
39254
|
+
const trace = (c, at) => this.store.memoryRowForTrace(c, at);
|
|
39255
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39256
|
+
const scoped = this.taintScope(m.channel_id);
|
|
39257
|
+
const strict = scoped && holdsManifestGrant(m.authorization);
|
|
39258
|
+
const strictRule = { ownerId, turnChannelId: m.channel_id };
|
|
39259
|
+
let tainted = false;
|
|
39260
|
+
const out = this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId, (i) => {
|
|
39261
|
+
const facts = { channelId: i.channelId, source: i.source, originId: i.originId, role: i.role, origin: i.origin, sourceRef: i.sourceRef };
|
|
39262
|
+
const keep = classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strict ? strictRule : void 0, !scoped) === "keep";
|
|
39263
|
+
if (keep && scoped && !strict && classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strictRule) === "exclude") tainted = true;
|
|
39264
|
+
return keep;
|
|
39265
|
+
});
|
|
39266
|
+
if (tainted) this.noteDeliveredFrame(m, true);
|
|
39267
|
+
return out;
|
|
37740
39268
|
}
|
|
37741
39269
|
/** ws-server: the hello echoes memory_unified only with the part 1 switch compiled on. */
|
|
37742
39270
|
unifiedFeatureOn() {
|
|
@@ -38038,6 +39566,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38038
39566
|
if (rec === void 0) return refuse("message");
|
|
38039
39567
|
const channelId = rec.channel_id;
|
|
38040
39568
|
if (rec.authorization?.memory_write !== true) return refuse("audience");
|
|
39569
|
+
if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
|
|
38041
39570
|
const held = await this.heldPosition(channelId);
|
|
38042
39571
|
const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
|
|
38043
39572
|
if (now.memory_write !== true) return refuse("audience");
|
|
@@ -38141,6 +39670,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38141
39670
|
if (rec === void 0) return refuse("message");
|
|
38142
39671
|
const channelId = rec.channel_id;
|
|
38143
39672
|
if (rec.authorization?.memory_write !== true) return refuse("audience");
|
|
39673
|
+
if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
|
|
38144
39674
|
const held = await this.heldPosition(channelId);
|
|
38145
39675
|
if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse("audience");
|
|
38146
39676
|
const scopeId = this.ownerScopeNow(channelId);
|
|
@@ -38224,6 +39754,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38224
39754
|
return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
|
|
38225
39755
|
}
|
|
38226
39756
|
async memoryContext(conversationId, limit) {
|
|
39757
|
+
for (const [id, channelId] of this.ownerVerifyFailedIds) {
|
|
39758
|
+
if (channelId === conversationId && this.runtimePendingIds.has(id)) return { recent: [], facts: [] };
|
|
39759
|
+
}
|
|
38227
39760
|
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
38228
39761
|
return { recent: [], facts: [] };
|
|
38229
39762
|
}
|
|
@@ -38371,7 +39904,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38371
39904
|
const fastLlm = this.fastLlm;
|
|
38372
39905
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
38373
39906
|
let memory;
|
|
38374
|
-
if (!this.
|
|
39907
|
+
if (!this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
38375
39908
|
memory = { recent: [], facts: [] };
|
|
38376
39909
|
} else {
|
|
38377
39910
|
try {
|
|
@@ -38419,15 +39952,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38419
39952
|
* frame (the "fast path failed" semantics), so a crash mid-call never loses
|
|
38420
39953
|
* the turn.
|
|
38421
39954
|
*/
|
|
38422
|
-
async stageHeldRuntimeMessage(
|
|
38423
|
-
if (this.runtimePendingIds.has(
|
|
39955
|
+
async stageHeldRuntimeMessage(held, opts = {}) {
|
|
39956
|
+
if (this.runtimePendingIds.has(held.message_id)) return true;
|
|
38424
39957
|
if (this.runtimePendingIds.size >= this.maxBuffer) {
|
|
38425
39958
|
writeServiceLog2("warn", "runtime_inbox.full");
|
|
38426
39959
|
return false;
|
|
38427
39960
|
}
|
|
39961
|
+
const message = opts.absorbed ? held : this.applyRoomSearchBackstop(held);
|
|
38428
39962
|
try {
|
|
38429
39963
|
await this.stageSealed(message);
|
|
38430
39964
|
this.runtimePendingIds.add(message.message_id);
|
|
39965
|
+
if (message !== held) this.backstopStripped.add(message.message_id);
|
|
38431
39966
|
return true;
|
|
38432
39967
|
} catch (error) {
|
|
38433
39968
|
writeServiceLog2("error", "runtime_inbox.stage_failed", {}, error);
|
|
@@ -38443,7 +39978,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38443
39978
|
}
|
|
38444
39979
|
const memory = await this.memoryAtRelease(message);
|
|
38445
39980
|
if (this.closingDown || !this.runtimePendingIds.has(message.message_id)) return;
|
|
38446
|
-
this.
|
|
39981
|
+
const out = this.backstopStripped.delete(message.message_id) ? this.withoutWebSearch(message) : message;
|
|
39982
|
+
this.buffer.push(memory === message.memory ? out : { ...out, memory });
|
|
38447
39983
|
this.wakeListeners();
|
|
38448
39984
|
}
|
|
38449
39985
|
/**
|
|
@@ -38461,7 +39997,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38461
39997
|
*/
|
|
38462
39998
|
async memoryAtRelease(message) {
|
|
38463
39999
|
const prior = message.memory;
|
|
38464
|
-
if (!prior || !this.
|
|
40000
|
+
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
38465
40001
|
let fresh;
|
|
38466
40002
|
try {
|
|
38467
40003
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -38469,7 +40005,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38469
40005
|
return prior;
|
|
38470
40006
|
}
|
|
38471
40007
|
const after = prior.recent.reduce((max, turn) => Math.max(max, turn.ts ?? 0), message.ts);
|
|
38472
|
-
const spoken = fresh.recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
|
|
40008
|
+
const spoken = this.fenceMemory(message.channel_id, fresh, holdsManifestGrant(message.authorization)).recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
|
|
38473
40009
|
if (spoken.length === 0) return prior;
|
|
38474
40010
|
return {
|
|
38475
40011
|
recent: [...prior.recent, ...spoken].slice(-_BridgeAgent.TURN_MEMORY_LIMIT),
|
|
@@ -38752,6 +40288,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38752
40288
|
});
|
|
38753
40289
|
}
|
|
38754
40290
|
this.channelRoster.set(channelId, { members, at: Date.now(), epoch, groupId, crossOrg: roomIsCrossOrg(rows) });
|
|
40291
|
+
const grantPin = this.cfg.ownerGrantPin?.pin;
|
|
40292
|
+
if (grantPin && !this.serverOwnerMismatchLogged && liveRows.some((r) => r.is_agent_owner === true && !r.is_agent && r.identity_id !== grantPin.ownerIdentityId)) {
|
|
40293
|
+
this.serverOwnerMismatchLogged = true;
|
|
40294
|
+
writeServiceLog2("warn", "authz.pin", { state: ServiceLogEnum.of("server_owner_mismatch", ["server_owner_mismatch"]) });
|
|
40295
|
+
}
|
|
38755
40296
|
const self = members.get(this.cfg.identityId);
|
|
38756
40297
|
if (self?.name) this.lastOwnDisplayName = self.name;
|
|
38757
40298
|
await this.resolveAndPinOwner(channelId, liveRows);
|
|
@@ -38778,11 +40319,200 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38778
40319
|
}
|
|
38779
40320
|
}
|
|
38780
40321
|
}
|
|
40322
|
+
/**
|
|
40323
|
+
* Owner-signed tool grants, step 1 (spec §7.2; decisions D-1, D-2, D-4, D-8,
|
|
40324
|
+
* D-9; audits H-1, M-2, M-4, E-1..E-3). The verdict for one delivered turn.
|
|
40325
|
+
*
|
|
40326
|
+
* `unchecked` when no pin is loaded or the sender is not the pinned owner.
|
|
40327
|
+
* Otherwise, in order, the first failure wins: the identity signature over the
|
|
40328
|
+
* raw payload bytes with the pinned key; the verified bytes must be the
|
|
40329
|
+
* decrypted envelope; in a DM only (D-1), the signed DM channel and the MLS
|
|
40330
|
+
* leaf set; then the replay set and closed epochs, keyed by the agent's OWN
|
|
40331
|
+
* held group (never the backend's row label, M-2). NEVER throws (M-4): any
|
|
40332
|
+
* exception is `failed verify_error`, and the row is acked like any other turn.
|
|
40333
|
+
*/
|
|
40334
|
+
async verifyOwnerTurn(m) {
|
|
40335
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40336
|
+
if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
|
|
40337
|
+
try {
|
|
40338
|
+
const sig = verifyOwnerSignature({ payloadHex: m.payloadHex, identitySignature: m.identitySignature }, pin);
|
|
40339
|
+
if (sig.state !== "ok") return this.ownerTurnFailed(m, this.isAckedOwnerOriginal(m) ? "replay" : sig.state);
|
|
40340
|
+
const v = sig.envelope;
|
|
40341
|
+
if (v.message_id !== m.envelope.message_id || v.channel_id !== m.envelope.channel_id || v.sender_id !== m.envelope.sender_id || v.mls_group_epoch !== m.envelope.mls_group_epoch || v.payload_encrypted !== m.envelope.payload_encrypted) {
|
|
40342
|
+
return this.ownerTurnFailed(m, "malformed");
|
|
40343
|
+
}
|
|
40344
|
+
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
40345
|
+
let leafClass = "ok";
|
|
40346
|
+
if (this.freshMeta(m.channelId)?.is_dm === true) {
|
|
40347
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
40348
|
+
if (signedDm !== void 0 && signedDm !== m.channelId) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
40349
|
+
leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
|
|
40350
|
+
ownerIdentityId: pin.ownerIdentityId,
|
|
40351
|
+
agentIdentityId: this.cfg.identityId,
|
|
40352
|
+
relayIdentityId: this.toolGrantManifest?.owner_dm_relay_identity_id,
|
|
40353
|
+
isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
|
|
40354
|
+
});
|
|
40355
|
+
if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
|
|
40356
|
+
}
|
|
40357
|
+
const held = await this.heldPosition(m.channelId);
|
|
40358
|
+
if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
|
|
40359
|
+
if (!this.runtimePendingIds.has(m.messageId) && (this.store.hasOwnerMessage(m.channelId, m.messageId) || this.store.isOwnerEpochClosed(m.channelId, held.groupId, v.mls_group_epoch))) {
|
|
40360
|
+
return this.ownerTurnFailed(m, "replay");
|
|
40361
|
+
}
|
|
40362
|
+
const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
|
|
40363
|
+
this.ownerReplayPending.set(m.messageId, replay);
|
|
40364
|
+
return Object.freeze({ kind: leafClass === "unsigned_relay" ? "constants_only" : "verified", replay });
|
|
40365
|
+
} catch {
|
|
40366
|
+
return this.ownerTurnFailed(m, "verify_error");
|
|
40367
|
+
}
|
|
40368
|
+
}
|
|
40369
|
+
/** Log, count and (except a replay, audit E-3) record one failed owner-turn check. Never throws (audit E-2). */
|
|
40370
|
+
ownerTurnFailed(m, reason) {
|
|
40371
|
+
writeServiceLog2("warn", "authz.owner_dm_verify", {
|
|
40372
|
+
state: ServiceLogEnum.of(reason, [
|
|
40373
|
+
"bad_signature",
|
|
40374
|
+
"malformed",
|
|
40375
|
+
"unknown_algo",
|
|
40376
|
+
"no_signature",
|
|
40377
|
+
"not_owner_dm_leaves",
|
|
40378
|
+
"wrong_dm_channel",
|
|
40379
|
+
"replay",
|
|
40380
|
+
"verify_error"
|
|
40381
|
+
])
|
|
40382
|
+
});
|
|
40383
|
+
this.ownerVerifyFailures += 1;
|
|
40384
|
+
if (reason !== "replay" && !this.isVerifiedOwnerOriginal(m)) {
|
|
40385
|
+
this.ownerVerifyFailedIds.set(m.messageId, m.channelId);
|
|
40386
|
+
try {
|
|
40387
|
+
this.store.recordOwnerVerifyFailure(m.channelId, m.messageId, Date.now());
|
|
40388
|
+
} catch {
|
|
40389
|
+
writeServiceLog2("warn", "authz.owner_verify_record_failed");
|
|
40390
|
+
}
|
|
40391
|
+
}
|
|
40392
|
+
return Object.freeze({ kind: "failed", reason });
|
|
40393
|
+
}
|
|
40394
|
+
/** Review M-A: a verified original of this id was staged and is still pending, or was recorded in the replay set. */
|
|
40395
|
+
isVerifiedOwnerOriginal(m) {
|
|
40396
|
+
return this.runtimePendingIds.has(m.messageId) || this.isAckedOwnerOriginal(m);
|
|
40397
|
+
}
|
|
40398
|
+
/** Review M-A: a verified original of this id was staged and acked (it is in the replay set, no longer pending). */
|
|
40399
|
+
isAckedOwnerOriginal(m) {
|
|
40400
|
+
if (this.runtimePendingIds.has(m.messageId)) return false;
|
|
40401
|
+
try {
|
|
40402
|
+
return this.store.hasOwnerMessage(m.channelId, m.messageId);
|
|
40403
|
+
} catch {
|
|
40404
|
+
return false;
|
|
40405
|
+
}
|
|
40406
|
+
}
|
|
40407
|
+
/** D-9: memory first, then the store (a restart before the reply). Unpinned: never. A store read that throws withholds (fail closed). */
|
|
40408
|
+
isOwnerVerifyFailed(messageId) {
|
|
40409
|
+
if (this.ownerVerifyFailedIds.has(messageId)) return true;
|
|
40410
|
+
if (!this.cfg.ownerGrantPin?.pin) return false;
|
|
40411
|
+
try {
|
|
40412
|
+
return this.store.isOwnerVerifyFailed(messageId);
|
|
40413
|
+
} catch {
|
|
40414
|
+
return true;
|
|
40415
|
+
}
|
|
40416
|
+
}
|
|
40417
|
+
/**
|
|
40418
|
+
* Decision D-2, audit L-2: prune the owner replay set at most once per
|
|
40419
|
+
* OWNER_REPLAY_PRUNE_INTERVAL_MS, keeping each channel's current (group,
|
|
40420
|
+
* epoch). A channel whose held position is unknown is left out (not pruned).
|
|
40421
|
+
*/
|
|
40422
|
+
async maybePruneOwnerReplay() {
|
|
40423
|
+
if (!this.cfg.ownerGrantPin?.pin) return;
|
|
40424
|
+
const now = this.monoNow();
|
|
40425
|
+
if (now - this.lastOwnerReplayPruneAt < OWNER_REPLAY_PRUNE_INTERVAL_MS) return;
|
|
40426
|
+
this.lastOwnerReplayPruneAt = now;
|
|
40427
|
+
try {
|
|
40428
|
+
const currentOf = /* @__PURE__ */ new Map();
|
|
40429
|
+
for (const channelId of this.store.listOwnerReplayChannels()) {
|
|
40430
|
+
const held = await this.heldPosition(channelId);
|
|
40431
|
+
if (held.groupId !== null && held.epoch !== null) currentOf.set(channelId, { groupId: held.groupId, epoch: held.epoch });
|
|
40432
|
+
}
|
|
40433
|
+
this.store.pruneOwnerReplay(Date.now(), currentOf);
|
|
40434
|
+
} catch (e) {
|
|
40435
|
+
writeServiceLog2("warn", "authz.replay_prune_failed", {}, e);
|
|
40436
|
+
}
|
|
40437
|
+
}
|
|
40438
|
+
/** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
|
|
40439
|
+
async loadToolGrantState() {
|
|
40440
|
+
const pin = this.cfg.ownerGrantPin?.pin;
|
|
40441
|
+
if (!pin) return;
|
|
40442
|
+
try {
|
|
40443
|
+
this.store.dropToolGrantManifestsExcept(pin.publicKeyB64);
|
|
40444
|
+
this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
|
|
40445
|
+
const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
|
|
40446
|
+
if (saved) {
|
|
40447
|
+
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
|
|
40448
|
+
if (v.state === "ok") {
|
|
40449
|
+
this.toolGrantManifest = v.manifest;
|
|
40450
|
+
this.toolGrantCounter = v.manifest.counter;
|
|
40451
|
+
} else {
|
|
40452
|
+
this.logManifest("malformed");
|
|
40453
|
+
}
|
|
40454
|
+
}
|
|
40455
|
+
} catch (e) {
|
|
40456
|
+
writeServiceLog2("warn", "authz.manifest_load_failed", {}, e);
|
|
40457
|
+
}
|
|
40458
|
+
await this.maybePruneOwnerReplay();
|
|
40459
|
+
}
|
|
40460
|
+
/**
|
|
40461
|
+
* Spec §7.1: verify and record a served manifest. Never writes a file and
|
|
40462
|
+
* never changes a grant in step 1. A save that throws keeps the old state.
|
|
40463
|
+
*/
|
|
40464
|
+
async applyToolGrantManifest(raw) {
|
|
40465
|
+
const pin = this.cfg.ownerGrantPin?.pin;
|
|
40466
|
+
if (!pin) {
|
|
40467
|
+
this.logManifest("no_pin");
|
|
40468
|
+
return;
|
|
40469
|
+
}
|
|
40470
|
+
if (raw === void 0 || raw === null) {
|
|
40471
|
+
this.logManifest("no_manifest");
|
|
40472
|
+
return;
|
|
40473
|
+
}
|
|
40474
|
+
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
|
|
40475
|
+
if (v.state === "ok") {
|
|
40476
|
+
try {
|
|
40477
|
+
await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
|
|
40478
|
+
} catch {
|
|
40479
|
+
this.logManifest("kept_last_good");
|
|
40480
|
+
return;
|
|
40481
|
+
}
|
|
40482
|
+
this.toolGrantManifest = v.manifest;
|
|
40483
|
+
this.toolGrantCounter = v.manifest.counter;
|
|
40484
|
+
this.logManifest("ok");
|
|
40485
|
+
if (v.droppedUnknown) this.logManifest("unknown_entry");
|
|
40486
|
+
return;
|
|
40487
|
+
}
|
|
40488
|
+
this.logManifest(v.state);
|
|
40489
|
+
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
40490
|
+
}
|
|
40491
|
+
logManifest(state) {
|
|
40492
|
+
this.manifestState = state;
|
|
40493
|
+
const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
|
|
40494
|
+
writeServiceLog2(quiet ? "info" : "warn", "authz.manifest", {
|
|
40495
|
+
state: ServiceLogEnum.of(state, [
|
|
40496
|
+
"ok",
|
|
40497
|
+
"unchanged",
|
|
40498
|
+
"no_pin",
|
|
40499
|
+
"no_manifest",
|
|
40500
|
+
"malformed",
|
|
40501
|
+
"unsupported_algo",
|
|
40502
|
+
"bad_signature",
|
|
40503
|
+
"wrong_agent",
|
|
40504
|
+
"wrong_owner",
|
|
40505
|
+
"rollback",
|
|
40506
|
+
"unknown_entry",
|
|
40507
|
+
"kept_last_good"
|
|
40508
|
+
])
|
|
40509
|
+
});
|
|
40510
|
+
}
|
|
38781
40511
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
38782
|
-
async authorizeTurn(channelId, messageId, senderId, relaySpeakerId) {
|
|
40512
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
38783
40513
|
const held = await this.heldPosition(channelId);
|
|
38784
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId);
|
|
38785
|
-
if (auth.tool_grants
|
|
40514
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
40515
|
+
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
38786
40516
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
38787
40517
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
38788
40518
|
return Object.freeze({ ...auth, tool_grants: NO_TOOL_GRANTS });
|
|
@@ -38864,9 +40594,48 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38864
40594
|
if (this.roomDefaultGrants && this.perToolConsumer !== null && epochFresh && meta?.is_dm === false && audience === "team_room" && entry.crossOrg === false) {
|
|
38865
40595
|
tool_grants = ROOM_DEFAULT_READ_GRANTS;
|
|
38866
40596
|
}
|
|
40597
|
+
let grantSource = tool_grants.length ? "constant" : "none";
|
|
40598
|
+
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
40599
|
+
const binding = this.manifestBinding;
|
|
40600
|
+
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40601
|
+
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40602
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40603
|
+
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
|
|
40604
|
+
const r = effectiveToolGrants({
|
|
40605
|
+
audience: "owner_dm",
|
|
40606
|
+
base: OWNER_DM_DEFAULT_READ_GRANTS,
|
|
40607
|
+
manifest,
|
|
40608
|
+
eligible,
|
|
40609
|
+
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
40610
|
+
disabled: this.payloadToolDeny,
|
|
40611
|
+
signedTargets,
|
|
40612
|
+
roomSearchCapped: false
|
|
40613
|
+
});
|
|
40614
|
+
tool_grants = r.grants;
|
|
40615
|
+
grantSource = r.source;
|
|
40616
|
+
} else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
|
|
40617
|
+
const r = effectiveToolGrants({
|
|
40618
|
+
audience: "team_room",
|
|
40619
|
+
base: ROOM_DEFAULT_READ_GRANTS,
|
|
40620
|
+
manifest,
|
|
40621
|
+
eligible: boundNow && this.manifestConsumer !== null,
|
|
40622
|
+
manifestSwitchOn: this.roomManifestGrants,
|
|
40623
|
+
disabled: this.payloadToolDeny,
|
|
40624
|
+
signedTargets,
|
|
40625
|
+
roomSearchCapped: manifest !== null && this.roomSearchCapped(channelId)
|
|
40626
|
+
});
|
|
40627
|
+
tool_grants = r.grants;
|
|
40628
|
+
grantSource = r.source;
|
|
40629
|
+
}
|
|
40630
|
+
if (opts.ownerVerdict?.kind === "failed") {
|
|
40631
|
+
tool_grants = NO_TOOL_GRANTS;
|
|
40632
|
+
memoryWrite = false;
|
|
40633
|
+
relaySpeaker = void 0;
|
|
40634
|
+
grantSource = "none";
|
|
40635
|
+
}
|
|
38867
40636
|
const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
|
|
38868
40637
|
if (opts.log !== false) {
|
|
38869
|
-
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite);
|
|
40638
|
+
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
|
|
38870
40639
|
}
|
|
38871
40640
|
return Object.freeze({
|
|
38872
40641
|
version: 1,
|
|
@@ -38902,8 +40671,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38902
40671
|
* enforcement is attached
|
|
38903
40672
|
* memory_write the turn carries memory_write (Markdown memory
|
|
38904
40673
|
* 7a, spec §8.3): a boolean, never content
|
|
40674
|
+
* grant_source where the grants came from: constant | none in
|
|
40675
|
+
* step 1 (owner-signed tool grants §7.6)
|
|
38905
40676
|
*/
|
|
38906
|
-
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite) {
|
|
40677
|
+
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite, grantSource) {
|
|
38907
40678
|
const meta = this.freshMeta(channelId);
|
|
38908
40679
|
const roster = this.freshRoster(channelId);
|
|
38909
40680
|
const sender = roster?.get(senderId);
|
|
@@ -38936,7 +40707,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38936
40707
|
epoch_fresh: epochFresh,
|
|
38937
40708
|
room_kill_switch: ServiceLogEnum.of(this.roomDefaultGrants ? "on" : "off", ["on", "off"]),
|
|
38938
40709
|
per_tool_consumer: this.perToolConsumer !== null,
|
|
38939
|
-
memory_write: memoryWrite
|
|
40710
|
+
memory_write: memoryWrite,
|
|
40711
|
+
grant_source: ServiceLogEnum.of(grantSource, ["constant", "manifest", "fallback", "none"])
|
|
38940
40712
|
});
|
|
38941
40713
|
}
|
|
38942
40714
|
/**
|
|
@@ -39307,7 +41079,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39307
41079
|
}
|
|
39308
41080
|
}
|
|
39309
41081
|
startReceivePump() {
|
|
39310
|
-
const sub = this.transport.subscribeDeliveryPings("");
|
|
41082
|
+
const sub = this.earlyPings ?? this.transport.subscribeDeliveryPings("");
|
|
41083
|
+
this.earlyPings = null;
|
|
39311
41084
|
this.pingHandle = { close: sub.close };
|
|
39312
41085
|
void (async () => {
|
|
39313
41086
|
try {
|
|
@@ -39529,6 +41302,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39529
41302
|
this.joinState.set(cid, "JOINED");
|
|
39530
41303
|
}
|
|
39531
41304
|
if (!entries.length) return;
|
|
41305
|
+
await this.maybePruneOwnerReplay();
|
|
39532
41306
|
const receivedAt = Date.now();
|
|
39533
41307
|
const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
|
|
39534
41308
|
const itemCaptureAtReceipt = /* @__PURE__ */ new Map();
|
|
@@ -39621,7 +41395,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39621
41395
|
acked.push(m.queueId);
|
|
39622
41396
|
continue;
|
|
39623
41397
|
}
|
|
39624
|
-
const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
|
|
41398
|
+
const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
|
|
39625
41399
|
await this.prepareNotesForTurn(m.channelId, refAuth);
|
|
39626
41400
|
const vaultRefMessage = {
|
|
39627
41401
|
channel_id: m.channelId,
|
|
@@ -39681,7 +41455,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39681
41455
|
{ attachmentsDir: this.attachmentsDirImpl, channelId: m.channelId, messageId: m.messageId },
|
|
39682
41456
|
this.receiveDeps()
|
|
39683
41457
|
);
|
|
39684
|
-
const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
|
|
41458
|
+
const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
|
|
39685
41459
|
await this.prepareNotesForTurn(m.channelId, shareAuth);
|
|
39686
41460
|
const runtimeMessage = {
|
|
39687
41461
|
channel_id: m.channelId,
|
|
@@ -39749,14 +41523,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39749
41523
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
39750
41524
|
m.envelope.sender_id
|
|
39751
41525
|
)?.name;
|
|
41526
|
+
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
39752
41527
|
const turnAuthorization = await this.authorizeTurn(
|
|
39753
41528
|
m.channelId,
|
|
39754
41529
|
m.messageId,
|
|
39755
41530
|
m.envelope.sender_id,
|
|
41531
|
+
ownerVerdict,
|
|
39756
41532
|
speaker?.identityId
|
|
39757
41533
|
);
|
|
39758
41534
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
39759
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
41535
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
39760
41536
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
39761
41537
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
39762
41538
|
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
@@ -39787,7 +41563,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39787
41563
|
sender_id: m.envelope.sender_id,
|
|
39788
41564
|
text: turnText,
|
|
39789
41565
|
ts: m.receivedAt,
|
|
39790
|
-
|
|
41566
|
+
// Owner grants plan 4 Task 16 (M-6): under a manifest a call-start greeting entry carries no grants and no
|
|
41567
|
+
// memory_write, on every branch (skip, held, heldAfterAck spreads held). Unpinned agents unchanged (G-4).
|
|
41568
|
+
authorization: fastGreeting && this.toolGrantManifest !== null ? greetingAuthorization(turnAuthorization) : turnAuthorization,
|
|
39791
41569
|
memory,
|
|
39792
41570
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
39793
41571
|
// name it after them and never flag it as agent-authored.
|
|
@@ -39811,7 +41589,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39811
41589
|
open2.ids.push(m.messageId);
|
|
39812
41590
|
open2.parts.push(turnText);
|
|
39813
41591
|
const staging = (async () => {
|
|
39814
|
-
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory));
|
|
41592
|
+
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory), { absorbed: true });
|
|
39815
41593
|
if (!ok) {
|
|
39816
41594
|
const i = open2.ids.indexOf(m.messageId);
|
|
39817
41595
|
if (i > 0) {
|
|
@@ -39950,11 +41728,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39950
41728
|
void work.finally(() => this.unifiedWork.delete(work));
|
|
39951
41729
|
}
|
|
39952
41730
|
}
|
|
39953
|
-
} else if (this.recallProviderOn()) {
|
|
41731
|
+
} else if (this.recallProviderOn() && ownerVerdict.kind !== "failed") {
|
|
39954
41732
|
const recallTap = this.captureRecallTap(m.messageId, m.channelId);
|
|
39955
41733
|
this.spawnRecallTap({ message: runtimeEntry(false, { recent: [], facts: [] }), captured: recallTap });
|
|
39956
41734
|
}
|
|
39957
|
-
void recordInboundMemory(
|
|
41735
|
+
if (ownerVerdict.kind !== "failed") void recordInboundMemory(
|
|
39958
41736
|
this.memory,
|
|
39959
41737
|
m.channelId,
|
|
39960
41738
|
m.envelope.sender_id,
|
|
@@ -39977,6 +41755,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39977
41755
|
} catch {
|
|
39978
41756
|
}
|
|
39979
41757
|
}
|
|
41758
|
+
this.ownerReplayPending.clear();
|
|
39980
41759
|
this.latency.record("recv.cycle", performance.now() - cycleStart);
|
|
39981
41760
|
if (this.buffer.length) {
|
|
39982
41761
|
for (const l of this.listeners) l();
|
|
@@ -40015,7 +41794,46 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40015
41794
|
return () => this.listeners.delete(listener);
|
|
40016
41795
|
}
|
|
40017
41796
|
/** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
|
|
41797
|
+
/** Review H-1: every buffer exit narrows manifest grants again to the state at delivery. */
|
|
40018
41798
|
takeBuffered(channelId) {
|
|
41799
|
+
return this.takeBufferedRaw(channelId).map((m) => this.delivering(m));
|
|
41800
|
+
}
|
|
41801
|
+
takeDeliverable(busyChannels) {
|
|
41802
|
+
return this.takeDeliverableRaw(busyChannels).map((m) => this.delivering(m));
|
|
41803
|
+
}
|
|
41804
|
+
/** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
|
|
41805
|
+
delivering(m) {
|
|
41806
|
+
const out = this.renarrowManifestGrants(m);
|
|
41807
|
+
this.noteDeliveredFrame(out);
|
|
41808
|
+
return out;
|
|
41809
|
+
}
|
|
41810
|
+
/**
|
|
41811
|
+
* Review H-1 (kill switches reach staged and restored turns): a grant marked source "manifest" survives delivery
|
|
41812
|
+
* only while its audience's manifest switch is on, the binding is bound at the applied counter, and the current
|
|
41813
|
+
* manifest still signs the tool. Otherwise it becomes the fallback: kept unmarked if it is a constant of the
|
|
41814
|
+
* audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
|
|
41815
|
+
*/
|
|
41816
|
+
renarrowManifestGrants(m) {
|
|
41817
|
+
const a = m.authorization;
|
|
41818
|
+
if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
|
|
41819
|
+
const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
|
|
41820
|
+
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
41821
|
+
const b = this.manifestBinding;
|
|
41822
|
+
const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
|
|
41823
|
+
const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
|
|
41824
|
+
const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
|
|
41825
|
+
const constants4 = new Set(audience === "owner_dm" ? OWNER_DM_DEFAULT_READ_GRANTS.map((g) => g.tool) : audience === "team_room" ? ROOM_DEFAULT_READ_GRANTS.map((g) => g.tool) : []);
|
|
41826
|
+
let changed = false;
|
|
41827
|
+
const out = a.tool_grants.flatMap((g) => {
|
|
41828
|
+
if (!(g.mode === "read" && g.source === "manifest")) return [g];
|
|
41829
|
+
if (switchOn && bound && signedNow.has(g.tool)) return [g];
|
|
41830
|
+
changed = true;
|
|
41831
|
+
return constants4.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
41832
|
+
});
|
|
41833
|
+
if (!changed) return m;
|
|
41834
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
|
|
41835
|
+
}
|
|
41836
|
+
takeBufferedRaw(channelId) {
|
|
40019
41837
|
if (channelId === void 0 && this.pausedChannels.size === 0 && this.supersededChannels.size === 0) {
|
|
40020
41838
|
return this.buffer.splice(0);
|
|
40021
41839
|
}
|
|
@@ -40038,7 +41856,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40038
41856
|
* working on — which it used to refuse with a "busy" reply and ack — and a
|
|
40039
41857
|
* backlog replays one message per channel at a time after a restart.
|
|
40040
41858
|
*/
|
|
40041
|
-
|
|
41859
|
+
takeDeliverableRaw(busyChannels) {
|
|
40042
41860
|
const picked = /* @__PURE__ */ new Set();
|
|
40043
41861
|
const out = [];
|
|
40044
41862
|
const keep = [];
|
|
@@ -40198,6 +42016,107 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40198
42016
|
}
|
|
40199
42017
|
this.noticeDeadLetter(message.channel_id);
|
|
40200
42018
|
}
|
|
42019
|
+
/** Spec §7.5: the room is capped today: cap 0, reached (by the runtime or the backstop), or at the cap. Fails closed. */
|
|
42020
|
+
roomSearchCapped(channelId) {
|
|
42021
|
+
if (this.roomSearchDailyCap === 0 || this.agentRoomSearchDailyCap === 0) return true;
|
|
42022
|
+
try {
|
|
42023
|
+
const day = utcDayOf(Date.now());
|
|
42024
|
+
const st = this.store.roomSearchCapState(channelId, day);
|
|
42025
|
+
if (this.store.roomSearchCapState(AGENT_TOTAL_KEY, day).grantedTurns >= this.agentRoomSearchDailyCap) return true;
|
|
42026
|
+
return st.reachedBy !== null || st.grantedTurns >= this.roomSearchDailyCap;
|
|
42027
|
+
} catch {
|
|
42028
|
+
return true;
|
|
42029
|
+
}
|
|
42030
|
+
}
|
|
42031
|
+
/** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
|
|
42032
|
+
noteRoomCapReached(channelId) {
|
|
42033
|
+
if (typeof channelId !== "string" || this.closingDown) return;
|
|
42034
|
+
if (this.roomSearchDailyCap === 0) return;
|
|
42035
|
+
if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
|
|
42036
|
+
const day = utcDayOf(Date.now());
|
|
42037
|
+
try {
|
|
42038
|
+
if (this.store.markRoomSearchCapReached(channelId, day, "runtime")) this.logRoomCap(day, channelId, "runtime");
|
|
42039
|
+
if (this.store.roomSearchCapState(channelId, day).noticePosted) return;
|
|
42040
|
+
} catch (e) {
|
|
42041
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
42042
|
+
return;
|
|
42043
|
+
}
|
|
42044
|
+
this.noticeRoomSearchCap(channelId, day);
|
|
42045
|
+
}
|
|
42046
|
+
/** K-14, once per room per UTC day; the flag is persisted only after the send resolves. Called only from noteRoomCapReached. */
|
|
42047
|
+
noticeRoomSearchCap(channelId, day) {
|
|
42048
|
+
const key = `${day}
|
|
42049
|
+
${channelId}`;
|
|
42050
|
+
if (this.roomCapNoticePending.has(key)) return;
|
|
42051
|
+
this.roomCapNoticePending.add(key);
|
|
42052
|
+
let pending;
|
|
42053
|
+
try {
|
|
42054
|
+
pending = this.send(channelId, ROOM_SEARCH_CAP_NOTICE, void 0, { agentTurn: false });
|
|
42055
|
+
} catch (e) {
|
|
42056
|
+
pending = Promise.reject(e);
|
|
42057
|
+
}
|
|
42058
|
+
void pending.then(
|
|
42059
|
+
() => {
|
|
42060
|
+
try {
|
|
42061
|
+
this.store.markRoomSearchCapNoticePosted(channelId, day);
|
|
42062
|
+
} catch (e) {
|
|
42063
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
42064
|
+
}
|
|
42065
|
+
writeServiceLog2("info", "authz.room_cap", { state: ServiceLogEnum.of("notice_sent", ["notice_sent"]) });
|
|
42066
|
+
},
|
|
42067
|
+
(e) => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("notice_failed", ["notice_failed"]) }, e)
|
|
42068
|
+
).finally(() => this.roomCapNoticePending.delete(key));
|
|
42069
|
+
}
|
|
42070
|
+
logRoomCap(day, channelId, by) {
|
|
42071
|
+
const key = `${day}
|
|
42072
|
+
${channelId}`;
|
|
42073
|
+
if (this.roomCapLogged.has(key)) return;
|
|
42074
|
+
if (this.roomCapLogged.size > 5e3) this.roomCapLogged.clear();
|
|
42075
|
+
this.roomCapLogged.add(key);
|
|
42076
|
+
writeServiceLog2("info", "authz.room_cap", {
|
|
42077
|
+
state: ServiceLogEnum.of("reached", ["reached"]),
|
|
42078
|
+
by: ServiceLogEnum.of(by, ["runtime", "bridge", "operator"])
|
|
42079
|
+
});
|
|
42080
|
+
}
|
|
42081
|
+
/**
|
|
42082
|
+
* Spec §7.5 (T-14b, audit r1 H-3): the silent bridge backstop. Counts a team_room turn that actually goes out
|
|
42083
|
+
* to the runtime with web_search, once per message id (callers run it after their runtimePendingIds early
|
|
42084
|
+
* return). Past the cap, and on a store failure (fail closed), the entry loses web_search. Never posts K-14.
|
|
42085
|
+
*/
|
|
42086
|
+
applyRoomSearchBackstop(entry) {
|
|
42087
|
+
const a = entry.authorization;
|
|
42088
|
+
if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
|
|
42089
|
+
const day = utcDayOf(Date.now());
|
|
42090
|
+
if (this.roomSearchDailyCap === 0) {
|
|
42091
|
+
this.logRoomCap(day, entry.channel_id, "operator");
|
|
42092
|
+
return this.withoutWebSearch(entry);
|
|
42093
|
+
}
|
|
42094
|
+
if (this.agentRoomSearchDailyCap === 0) {
|
|
42095
|
+
this.logRoomCap(day, AGENT_TOTAL_KEY, "operator");
|
|
42096
|
+
return this.withoutWebSearch(entry);
|
|
42097
|
+
}
|
|
42098
|
+
try {
|
|
42099
|
+
const n = this.store.countRoomSearchGrant(entry.channel_id, day);
|
|
42100
|
+
if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
|
|
42101
|
+
this.logRoomCap(day, entry.channel_id, "bridge");
|
|
42102
|
+
}
|
|
42103
|
+
if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
|
|
42104
|
+
const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
|
|
42105
|
+
if (total === this.agentRoomSearchDailyCap) this.logRoomCap(day, AGENT_TOTAL_KEY, "bridge");
|
|
42106
|
+
if (total > this.agentRoomSearchDailyCap) return this.withoutWebSearch(entry);
|
|
42107
|
+
return entry;
|
|
42108
|
+
} catch (e) {
|
|
42109
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("count_failed", ["count_failed"]) }, e);
|
|
42110
|
+
return this.withoutWebSearch(entry);
|
|
42111
|
+
}
|
|
42112
|
+
}
|
|
42113
|
+
withoutWebSearch(entry) {
|
|
42114
|
+
const a = entry.authorization;
|
|
42115
|
+
return {
|
|
42116
|
+
...entry,
|
|
42117
|
+
authorization: Object.freeze({ ...a, tool_grants: Object.freeze(a.tool_grants.filter((g) => g.tool !== "web_search")) })
|
|
42118
|
+
};
|
|
42119
|
+
}
|
|
40201
42120
|
noticeDeadLetter(channelId) {
|
|
40202
42121
|
if (this.closingDown) return;
|
|
40203
42122
|
const now = Date.now();
|
|
@@ -40419,13 +42338,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40419
42338
|
modeEpoch: this.store.recallState().modeEpoch
|
|
40420
42339
|
};
|
|
40421
42340
|
let persisted = { stamp, recallPurgeEpoch: recall.purgeEpoch, recallModeEpoch: recall.modeEpoch };
|
|
40422
|
-
|
|
40423
|
-
|
|
40424
|
-
|
|
40425
|
-
|
|
40426
|
-
|
|
40427
|
-
|
|
40428
|
-
|
|
42341
|
+
const replay = this.ownerReplayPending.get(message.message_id);
|
|
42342
|
+
try {
|
|
42343
|
+
await this.store.stageRuntimeInbox(message.message_id, this.stagedPayload(message, stamp, recall), message.ts, (prior, next) => {
|
|
42344
|
+
const a = splitIngestStamp(prior);
|
|
42345
|
+
const b = splitIngestStamp(next);
|
|
42346
|
+
if (a === null || b === null || a.message !== b.message) return false;
|
|
42347
|
+
persisted = a.marks;
|
|
42348
|
+
return true;
|
|
42349
|
+
}, replay ? { ...replay, processedAt: Date.now() } : void 0);
|
|
42350
|
+
} finally {
|
|
42351
|
+
this.ownerReplayPending.delete(message.message_id);
|
|
42352
|
+
}
|
|
40429
42353
|
this.applyStagedMarks(message.message_id, persisted);
|
|
40430
42354
|
}
|
|
40431
42355
|
/** In-memory mirror of the persisted marks (staging and restore). */
|
|
@@ -40445,6 +42369,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40445
42369
|
const message = rest;
|
|
40446
42370
|
this.applyStagedMarks(message.message_id, marks);
|
|
40447
42371
|
this.runtimePendingIds.add(message.message_id);
|
|
42372
|
+
if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
|
|
42373
|
+
this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
|
|
42374
|
+
}
|
|
40448
42375
|
if (this.unifiedOn) this.deferredUnifiedIds.add(message.message_id);
|
|
40449
42376
|
else this.deferredRecallIds.add(message.message_id);
|
|
40450
42377
|
if (row.deliveryAttempts > 0) {
|
|
@@ -40461,12 +42388,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40461
42388
|
* Returning false is backpressure: the caller leaves the backend row unacked
|
|
40462
42389
|
* so it remains the durable queue rather than shedding accepted work.
|
|
40463
42390
|
*/
|
|
40464
|
-
async stageRuntimeMessage(
|
|
40465
|
-
if (this.runtimePendingIds.has(
|
|
42391
|
+
async stageRuntimeMessage(staging, opts = {}) {
|
|
42392
|
+
if (this.runtimePendingIds.has(staging.message_id)) return true;
|
|
40466
42393
|
if (this.runtimePendingIds.size >= this.maxBuffer) {
|
|
40467
42394
|
writeServiceLog2("warn", "runtime_inbox.full");
|
|
40468
42395
|
return false;
|
|
40469
42396
|
}
|
|
42397
|
+
const message = opts.absorbed ? staging : this.applyRoomSearchBackstop(staging);
|
|
40470
42398
|
try {
|
|
40471
42399
|
await this.stageSealed(message);
|
|
40472
42400
|
this.runtimePendingIds.add(message.message_id);
|
|
@@ -40572,6 +42500,37 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40572
42500
|
perToolConsumerAttached() {
|
|
40573
42501
|
return this.perToolConsumer !== null;
|
|
40574
42502
|
}
|
|
42503
|
+
/**
|
|
42504
|
+
* Owner grants (spec §7.9, plan 4 Task 5): the attached per-tool runtime socket whose hello
|
|
42505
|
+
* declared manifest_grants_v1, or null. A manifest grant is issued only while one is attached
|
|
42506
|
+
* (Task 13); every other socket receives manifest grants narrowed to the constants.
|
|
42507
|
+
* Newest wins, like perToolConsumer.
|
|
42508
|
+
*/
|
|
42509
|
+
manifestConsumer = null;
|
|
42510
|
+
/** OQ-5: the latest hello's class; "missing" until any hello. Reported in tool_grant_status (Task 15). */
|
|
42511
|
+
lastConsumerCapability = "missing";
|
|
42512
|
+
noteManifestConsumer(token) {
|
|
42513
|
+
this.manifestConsumer = token;
|
|
42514
|
+
}
|
|
42515
|
+
clearManifestConsumer(token) {
|
|
42516
|
+
if (this.manifestConsumer === token) this.manifestConsumer = null;
|
|
42517
|
+
}
|
|
42518
|
+
manifestConsumerAttached() {
|
|
42519
|
+
return this.manifestConsumer !== null;
|
|
42520
|
+
}
|
|
42521
|
+
noteConsumerCapability(state) {
|
|
42522
|
+
this.lastConsumerCapability = state;
|
|
42523
|
+
if (state !== "ok") {
|
|
42524
|
+
writeServiceLog2("info", "authz.capability", { state: ServiceLogEnum.of(state, ["missing", "harness_consumer"]) });
|
|
42525
|
+
}
|
|
42526
|
+
}
|
|
42527
|
+
/**
|
|
42528
|
+
* Harness re-rule (2026-10-12, audit r2 H-3): true exactly when this agent has a pin loaded and a good
|
|
42529
|
+
* manifest. A harness consumer then receives [] in every audience (consumer-grants.ts withHarnessGrants).
|
|
42530
|
+
*/
|
|
42531
|
+
harnessGrantsWithheld() {
|
|
42532
|
+
return this.cfg.ownerGrantPin?.pin != null && this.toolGrantManifest !== null;
|
|
42533
|
+
}
|
|
40575
42534
|
health() {
|
|
40576
42535
|
let joined = 0;
|
|
40577
42536
|
for (const s of this.joinState.values()) if (s === "JOINED") joined++;
|
|
@@ -40706,7 +42665,42 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40706
42665
|
build: BRIDGE_VERSION
|
|
40707
42666
|
};
|
|
40708
42667
|
}
|
|
40709
|
-
|
|
42668
|
+
/** C9: `session_superseded` once a 4409 said another copy took this
|
|
42669
|
+
* agent's session (the gated fetch already refuses every request). The
|
|
42670
|
+
* local HTTP routes read it: every route but `/healthz` answers 503. */
|
|
42671
|
+
get sessionState() {
|
|
42672
|
+
return this.sessionSuperseded ? "session_superseded" : "live";
|
|
42673
|
+
}
|
|
42674
|
+
/** C9: subscribe to `session_superseded`. runMode subscribes right after
|
|
42675
|
+
* construction, before start(), so a 4409 during start() is heard too. */
|
|
42676
|
+
on(event, listener) {
|
|
42677
|
+
this.sessionSupersededListeners.add(listener);
|
|
42678
|
+
return () => this.sessionSupersededListeners.delete(listener);
|
|
42679
|
+
}
|
|
42680
|
+
/** P-1 step 0, then the event (spec §4.1.6): another copy of this agent took
|
|
42681
|
+
* the session (4409). The gated-fetch switch is set FIRST, so no HTTP
|
|
42682
|
+
* request leaves the process after this, whatever a listener does. */
|
|
42683
|
+
onSessionSuperseded() {
|
|
42684
|
+
this.sessionSuperseded = true;
|
|
42685
|
+
for (const listener of [...this.sessionSupersededListeners]) {
|
|
42686
|
+
try {
|
|
42687
|
+
listener();
|
|
42688
|
+
} catch (e) {
|
|
42689
|
+
writeServiceLog2("error", "transport.session_superseded_listener_failed", {}, e);
|
|
42690
|
+
}
|
|
42691
|
+
}
|
|
42692
|
+
}
|
|
42693
|
+
/**
|
|
42694
|
+
* Idempotent (C9, spec §4.1.6 low 3): every call returns the FIRST call's
|
|
42695
|
+
* promise, so a SIGTERM during or after the supersede quiesce never runs
|
|
42696
|
+
* store.close() twice. A rejected close stays rejected: a retry would close
|
|
42697
|
+
* the store a second time.
|
|
42698
|
+
*/
|
|
42699
|
+
close() {
|
|
42700
|
+
this.closing ??= this.closeOnce();
|
|
42701
|
+
return this.closing;
|
|
42702
|
+
}
|
|
42703
|
+
async closeOnce() {
|
|
40710
42704
|
this.closingDown = true;
|
|
40711
42705
|
this.statusGate.close();
|
|
40712
42706
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
@@ -40719,6 +42713,28 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40719
42713
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
40720
42714
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
40721
42715
|
if (this.membershipSweepTimer) clearInterval(this.membershipSweepTimer);
|
|
42716
|
+
const ingest = this.ingestInFlight;
|
|
42717
|
+
if (ingest) {
|
|
42718
|
+
let timer;
|
|
42719
|
+
await Promise.race([
|
|
42720
|
+
ingest.released,
|
|
42721
|
+
new Promise((resolve) => {
|
|
42722
|
+
timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
42723
|
+
})
|
|
42724
|
+
]);
|
|
42725
|
+
if (timer) clearTimeout(timer);
|
|
42726
|
+
}
|
|
42727
|
+
const starting = this.startInFlight;
|
|
42728
|
+
if (starting) {
|
|
42729
|
+
let timer;
|
|
42730
|
+
await Promise.race([
|
|
42731
|
+
starting,
|
|
42732
|
+
new Promise((resolve) => {
|
|
42733
|
+
timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
42734
|
+
})
|
|
42735
|
+
]);
|
|
42736
|
+
if (timer) clearTimeout(timer);
|
|
42737
|
+
}
|
|
40722
42738
|
if (this.membershipSweepWork) {
|
|
40723
42739
|
let timer;
|
|
40724
42740
|
await Promise.race([
|
|
@@ -40802,11 +42818,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40802
42818
|
};
|
|
40803
42819
|
|
|
40804
42820
|
// src/config.ts
|
|
40805
|
-
import { readFileSync as
|
|
40806
|
-
import { dirname as
|
|
42821
|
+
import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
|
|
42822
|
+
import { dirname as dirname6, join as join18 } from "node:path";
|
|
40807
42823
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
40808
42824
|
function loadConfig(opts) {
|
|
40809
|
-
const raw =
|
|
42825
|
+
const raw = readFileSync10(opts.credentialPath, "utf8");
|
|
40810
42826
|
try {
|
|
40811
42827
|
const mode = statSync5(opts.credentialPath).mode & 511;
|
|
40812
42828
|
if (mode & 63) writeServiceLog2("warn", "config.credential_permissions_unsafe");
|
|
@@ -40819,7 +42835,11 @@ function loadConfig(opts) {
|
|
|
40819
42835
|
throw new Error("credential file is not valid JSON");
|
|
40820
42836
|
}
|
|
40821
42837
|
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
|
|
40822
|
-
const storePath = opts.storePath ??
|
|
42838
|
+
const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
|
|
42839
|
+
const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
|
|
42840
|
+
writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
|
|
42841
|
+
state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
|
|
42842
|
+
});
|
|
40823
42843
|
const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
|
|
40824
42844
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
40825
42845
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
@@ -40877,7 +42897,8 @@ function loadConfig(opts) {
|
|
|
40877
42897
|
voice,
|
|
40878
42898
|
voiceFastPathMode: parseFastPathMode(json2.voice_fast_path_mode),
|
|
40879
42899
|
modelSource,
|
|
40880
|
-
memoryEnabled
|
|
42900
|
+
memoryEnabled,
|
|
42901
|
+
ownerGrantPin
|
|
40881
42902
|
};
|
|
40882
42903
|
}
|
|
40883
42904
|
function arg(argv, flag) {
|
|
@@ -40921,11 +42942,11 @@ import { z as z3 } from "zod";
|
|
|
40921
42942
|
|
|
40922
42943
|
// src/auth.ts
|
|
40923
42944
|
import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
40924
|
-
import { existsSync as
|
|
42945
|
+
import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
|
|
40925
42946
|
function resolveToken(opts) {
|
|
40926
42947
|
if (opts.token) return opts.token;
|
|
40927
42948
|
const f = opts.tokenFile;
|
|
40928
|
-
if (f &&
|
|
42949
|
+
if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
|
|
40929
42950
|
const tok = randomBytes11(32).toString("base64url");
|
|
40930
42951
|
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
40931
42952
|
return tok;
|
|
@@ -40990,6 +43011,9 @@ var BRIDGE_ERRORS = {
|
|
|
40990
43011
|
// longer holds the shared bytes or key. Terminal for that link: never fall
|
|
40991
43012
|
// back to the current version. `detail.pin` names the refused pin.
|
|
40992
43013
|
vault_ref_changed: { http: 422, message: "this file changed in Files after it was shared; not opened" },
|
|
43014
|
+
// C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
|
|
43015
|
+
// session (WS 4409), so this copy stopped. Every local route but /healthz.
|
|
43016
|
+
session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
|
|
40993
43017
|
internal: { http: 500, message: "internal error" }
|
|
40994
43018
|
};
|
|
40995
43019
|
function errorBody(code, detail) {
|
|
@@ -41017,6 +43041,29 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
41017
43041
|
const { memory_write: _dropped, ...rest } = a;
|
|
41018
43042
|
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
41019
43043
|
}
|
|
43044
|
+
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
43045
|
+
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
43046
|
+
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
43047
|
+
team_room: Object.freeze(["current_time"])
|
|
43048
|
+
});
|
|
43049
|
+
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
43050
|
+
function withManifestCapability(m, manifestConsumer) {
|
|
43051
|
+
if (manifestConsumer) return m;
|
|
43052
|
+
const a = m.authorization;
|
|
43053
|
+
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
43054
|
+
const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
43055
|
+
const narrowed = a.tool_grants.flatMap((g) => {
|
|
43056
|
+
if (!isManifestGrant(g)) return [g];
|
|
43057
|
+
return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
43058
|
+
});
|
|
43059
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
43060
|
+
}
|
|
43061
|
+
function withHarnessGrants(m, withheld) {
|
|
43062
|
+
if (!withheld) return m;
|
|
43063
|
+
const a = m.authorization;
|
|
43064
|
+
if (!a || a.tool_grants.length === 0) return m;
|
|
43065
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
43066
|
+
}
|
|
41020
43067
|
|
|
41021
43068
|
// src/http-server.ts
|
|
41022
43069
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -41038,7 +43085,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
41038
43085
|
return [];
|
|
41039
43086
|
}
|
|
41040
43087
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
41041
|
-
const
|
|
43088
|
+
const withheld = agent.harnessGrantsWithheld();
|
|
43089
|
+
const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
|
|
41042
43090
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
41043
43091
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
41044
43092
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -41196,6 +43244,10 @@ function createHttpServer(agent, opts) {
|
|
|
41196
43244
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
41197
43245
|
const path = url.pathname;
|
|
41198
43246
|
const method = req.method ?? "GET";
|
|
43247
|
+
if (agent.sessionState === "session_superseded") {
|
|
43248
|
+
if (method === "GET" && path === "/healthz") return json(res, 200, { state: "session_superseded" });
|
|
43249
|
+
return jsonError(res, "session_superseded");
|
|
43250
|
+
}
|
|
41199
43251
|
if (method === "GET" && path === "/healthz") {
|
|
41200
43252
|
return json(res, 200, { ...agent.health(), capabilities: capabilities(opts) });
|
|
41201
43253
|
}
|
|
@@ -41305,7 +43357,7 @@ function createHttpServer(agent, opts) {
|
|
|
41305
43357
|
if (method === "GET" && path === "/v1/messages") {
|
|
41306
43358
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
41307
43359
|
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|
|
41308
|
-
return json(res, 200, (await agent.drain(channel, wait)).map((m) => withConsumerGrants(m, false)));
|
|
43360
|
+
return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
|
|
41309
43361
|
}
|
|
41310
43362
|
if (method === "GET" && path === "/v1/events") {
|
|
41311
43363
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -41407,6 +43459,7 @@ var PING_INTERVAL_MS = 3e4;
|
|
|
41407
43459
|
var INFLIGHT_STALE_SWEEP_MS = 6e4;
|
|
41408
43460
|
var INFLIGHT_STALE_AFTER_MS = 15 * 6e4;
|
|
41409
43461
|
var HELLO_WAIT_MS = 2e3;
|
|
43462
|
+
var WS_CLOSE_SESSION_SUPERSEDED3 = 4409;
|
|
41410
43463
|
var MEMORY_NOTES_FEATURE = "memory_notes";
|
|
41411
43464
|
var MEMORY_RECALL_FEATURE = "memory_recall";
|
|
41412
43465
|
var MEMORY_UNIFIED_FEATURE = "memory_unified";
|
|
@@ -41427,7 +43480,12 @@ function authedUpgrade(req, opts) {
|
|
|
41427
43480
|
return true;
|
|
41428
43481
|
}
|
|
41429
43482
|
function attachAgentWebSocket(server, agent, opts) {
|
|
41430
|
-
|
|
43483
|
+
let sessionSuperseded = false;
|
|
43484
|
+
const wss = new WebSocketServer({
|
|
43485
|
+
server,
|
|
43486
|
+
path: "/v1/ws",
|
|
43487
|
+
verifyClient: (_info, done) => sessionSuperseded ? done(false, 503, "session_superseded") : done(true)
|
|
43488
|
+
});
|
|
41431
43489
|
const keepalive = setInterval(() => {
|
|
41432
43490
|
for (const client of wss.clients) {
|
|
41433
43491
|
const sock = client;
|
|
@@ -41472,6 +43530,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41472
43530
|
const session = {
|
|
41473
43531
|
consumer: "unknown",
|
|
41474
43532
|
perToolGrants: false,
|
|
43533
|
+
manifestGrants: false,
|
|
41475
43534
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
41476
43535
|
deliveryOpen: helloWaitMs <= 0,
|
|
41477
43536
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -41524,7 +43583,15 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41524
43583
|
retired = true;
|
|
41525
43584
|
continue;
|
|
41526
43585
|
}
|
|
41527
|
-
const out = withConsumerNotes(
|
|
43586
|
+
const out = withConsumerNotes(
|
|
43587
|
+
withManifestCapability(
|
|
43588
|
+
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
43589
|
+
session.manifestGrants
|
|
43590
|
+
),
|
|
43591
|
+
batch[i],
|
|
43592
|
+
session,
|
|
43593
|
+
agent
|
|
43594
|
+
);
|
|
41528
43595
|
const recallOut = withConsumerRecall(out.message, batch[i], session, agent);
|
|
41529
43596
|
const unifiedOut = withConsumerUnified(recallOut.message, batch[i], session, agent);
|
|
41530
43597
|
const frame = { type: "message", ...unifiedOut.message };
|
|
@@ -41558,6 +43625,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41558
43625
|
if (helloTimer) clearTimeout(helloTimer);
|
|
41559
43626
|
agent.noteSessionClose();
|
|
41560
43627
|
unsubscribe();
|
|
43628
|
+
if (sessionSuperseded) inflight.clear();
|
|
41561
43629
|
if (inflight.size) {
|
|
41562
43630
|
agent.requeueFailedRuntimeDeliveries(
|
|
41563
43631
|
[...inflight.values()].filter((m) => !agent.isRuntimeMessageRetired(m.message_id))
|
|
@@ -41573,12 +43641,16 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41573
43641
|
if (current?.sock === sock) current = null;
|
|
41574
43642
|
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
41575
43643
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
43644
|
+
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
41576
43645
|
};
|
|
41577
43646
|
current = { sock, cleanup, sweepStale };
|
|
41578
43647
|
sock.on("close", cleanup);
|
|
41579
43648
|
sock.on("error", cleanup);
|
|
41580
43649
|
sock.on("message", (data) => {
|
|
41581
|
-
|
|
43650
|
+
if (sessionSuperseded) return;
|
|
43651
|
+
handleInbound(sock, agent, data, inflight, flush, session).catch((e) => {
|
|
43652
|
+
writeServiceLog2("error", "ws.inbound_failed", {}, e);
|
|
43653
|
+
});
|
|
41582
43654
|
});
|
|
41583
43655
|
flush();
|
|
41584
43656
|
});
|
|
@@ -41593,6 +43665,13 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41593
43665
|
}
|
|
41594
43666
|
}
|
|
41595
43667
|
wss.close();
|
|
43668
|
+
},
|
|
43669
|
+
/** C9 (spec §4.1.6 step 1, OQ-9): close every open socket with 4409 /
|
|
43670
|
+
* `session_superseded`, and answer every later upgrade with 503. The
|
|
43671
|
+
* runtime's reconnect backoff does the rest; idempotent. */
|
|
43672
|
+
enterSessionSuperseded() {
|
|
43673
|
+
sessionSuperseded = true;
|
|
43674
|
+
for (const client of wss.clients) client.close(WS_CLOSE_SESSION_SUPERSEDED3, "session_superseded");
|
|
41596
43675
|
}
|
|
41597
43676
|
};
|
|
41598
43677
|
}
|
|
@@ -41652,6 +43731,15 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41652
43731
|
session.perToolGrants = false;
|
|
41653
43732
|
agent.clearPerToolConsumer(session);
|
|
41654
43733
|
}
|
|
43734
|
+
const manifest = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE);
|
|
43735
|
+
if (manifest && !session.manifestGrants) {
|
|
43736
|
+
session.manifestGrants = true;
|
|
43737
|
+
agent.noteManifestConsumer(session);
|
|
43738
|
+
} else if (!manifest && session.manifestGrants) {
|
|
43739
|
+
session.manifestGrants = false;
|
|
43740
|
+
agent.clearManifestConsumer(session);
|
|
43741
|
+
}
|
|
43742
|
+
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
41655
43743
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
41656
43744
|
agent.noteNotesConsumer();
|
|
41657
43745
|
}
|
|
@@ -41659,7 +43747,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41659
43747
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
41660
43748
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
41661
43749
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
41662
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : []] });
|
|
43750
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
|
|
41663
43751
|
if (!session.deliveryOpen) {
|
|
41664
43752
|
session.deliveryOpen = true;
|
|
41665
43753
|
flush();
|
|
@@ -41872,8 +43960,17 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41872
43960
|
}
|
|
41873
43961
|
return;
|
|
41874
43962
|
}
|
|
43963
|
+
if (m.type === "room_cap_reached") {
|
|
43964
|
+
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
43965
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
43966
|
+
return;
|
|
43967
|
+
}
|
|
43968
|
+
agent.noteRoomCapReached(m.channel_id);
|
|
43969
|
+
return;
|
|
43970
|
+
}
|
|
41875
43971
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
41876
43972
|
}
|
|
43973
|
+
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
41877
43974
|
export {
|
|
41878
43975
|
BRIDGE_ERRORS,
|
|
41879
43976
|
BRIDGE_VERSION,
|