@securecoms/agent-bridge 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -9
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +2486 -469
- package/dist/bridge-agent.d.ts +209 -2
- package/dist/config.d.ts +7 -0
- package/dist/consumer-grants.d.ts +19 -0
- 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 +1994 -171
- 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/runtime-config-dir.d.ts +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +1 -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);
|
|
@@ -25542,7 +25544,9 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25542
25544
|
epoch: p.epoch,
|
|
25543
25545
|
payload: bytesToHex22(canonical),
|
|
25544
25546
|
connect_channel_id: p.channelId,
|
|
25545
|
-
created_at: ""
|
|
25547
|
+
created_at: "",
|
|
25548
|
+
identity_signature: null
|
|
25549
|
+
// the envelope above is unsigned
|
|
25546
25550
|
});
|
|
25547
25551
|
}
|
|
25548
25552
|
}
|
|
@@ -25554,7 +25558,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25554
25558
|
// edge stores the canonical envelope bytes (hex) and fans out one delivery row
|
|
25555
25559
|
// per active member device except the sender's. We resolve the sender's device
|
|
25556
25560
|
// from sender_id (recorded at registerDevice); if unknown, every member gets it.
|
|
25557
|
-
|
|
25561
|
+
//
|
|
25562
|
+
// Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
|
|
25563
|
+
// the envelope's `identity_proof.signature` as `identity_signature` beside the
|
|
25564
|
+
// unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
|
|
25565
|
+
async ingestApplication(envelope, opts) {
|
|
25558
25566
|
const channelId = envelope.channel_id;
|
|
25559
25567
|
const canonical = await canonicalEnvelopeBytes(envelope);
|
|
25560
25568
|
const members = this.membersByChannel.get(channelId) ?? [];
|
|
@@ -25572,7 +25580,8 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
25572
25580
|
epoch: envelope.mls_group_epoch,
|
|
25573
25581
|
payload: bytesToHex22(canonical),
|
|
25574
25582
|
connect_channel_id: channelId,
|
|
25575
|
-
created_at: ""
|
|
25583
|
+
created_at: "",
|
|
25584
|
+
identity_signature: opts?.omitSignature ? null : envelope.identity_proof.signature || null
|
|
25576
25585
|
});
|
|
25577
25586
|
}
|
|
25578
25587
|
}
|
|
@@ -25944,11 +25953,15 @@ function openSchema(db) {
|
|
|
25944
25953
|
ensureUnifiedDmOwner(db);
|
|
25945
25954
|
ensureConversationLastAbsentAt(db);
|
|
25946
25955
|
ensureRecallForgetTables(db);
|
|
25956
|
+
ensureAgentMemoryOrigin(db);
|
|
25947
25957
|
ensureRuntimeInboxDeliveryAttempts(db);
|
|
25948
25958
|
ensureRuntimeReplyIntent(db);
|
|
25949
25959
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
25950
25960
|
ensureGroupLink(db);
|
|
25951
25961
|
ensureMlsCustody(db);
|
|
25962
|
+
ensureToolGrantTables(db);
|
|
25963
|
+
ensureToolGrantAppliedTable(db);
|
|
25964
|
+
ensureRoomSearchCapTable(db);
|
|
25952
25965
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
25953
25966
|
}
|
|
25954
25967
|
function ensureMlsCustody(db) {
|
|
@@ -26043,6 +26056,11 @@ function ensureRecallStateColumns(db) {
|
|
|
26043
26056
|
}
|
|
26044
26057
|
}
|
|
26045
26058
|
}
|
|
26059
|
+
function ensureAgentMemoryOrigin(db) {
|
|
26060
|
+
const cols = db.prepare("PRAGMA table_info(agent_memory)").all();
|
|
26061
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "origin"))
|
|
26062
|
+
db.exec("ALTER TABLE agent_memory ADD COLUMN origin TEXT");
|
|
26063
|
+
}
|
|
26046
26064
|
function ensureRecallForgetTables(db) {
|
|
26047
26065
|
db.exec(`
|
|
26048
26066
|
CREATE TABLE IF NOT EXISTS agent_memory_recall_forgotten (
|
|
@@ -26118,6 +26136,67 @@ function ensureUnifiedStateColumns(db) {
|
|
|
26118
26136
|
}
|
|
26119
26137
|
}
|
|
26120
26138
|
}
|
|
26139
|
+
function ensureToolGrantAppliedTable(db) {
|
|
26140
|
+
db.exec(`
|
|
26141
|
+
CREATE TABLE IF NOT EXISTS tool_grant_applied (
|
|
26142
|
+
identity_id TEXT NOT NULL,
|
|
26143
|
+
pin_key_b64 TEXT NOT NULL,
|
|
26144
|
+
sealed_applied BLOB NOT NULL,
|
|
26145
|
+
updated_at INTEGER NOT NULL,
|
|
26146
|
+
PRIMARY KEY (identity_id, pin_key_b64)
|
|
26147
|
+
);
|
|
26148
|
+
`);
|
|
26149
|
+
}
|
|
26150
|
+
function ensureRoomSearchCapTable(db) {
|
|
26151
|
+
db.exec(`
|
|
26152
|
+
CREATE TABLE IF NOT EXISTS room_search_cap (
|
|
26153
|
+
identity_id TEXT NOT NULL,
|
|
26154
|
+
channel_id TEXT NOT NULL,
|
|
26155
|
+
utc_day TEXT NOT NULL,
|
|
26156
|
+
granted_turns INTEGER NOT NULL,
|
|
26157
|
+
reached_by TEXT,
|
|
26158
|
+
notice_posted INTEGER NOT NULL,
|
|
26159
|
+
PRIMARY KEY (identity_id, channel_id)
|
|
26160
|
+
);
|
|
26161
|
+
`);
|
|
26162
|
+
}
|
|
26163
|
+
function ensureToolGrantTables(db) {
|
|
26164
|
+
db.exec(`
|
|
26165
|
+
CREATE TABLE IF NOT EXISTS tool_grant_manifest (
|
|
26166
|
+
identity_id TEXT NOT NULL,
|
|
26167
|
+
pin_key_b64 TEXT NOT NULL,
|
|
26168
|
+
counter INTEGER NOT NULL,
|
|
26169
|
+
sealed_manifest BLOB NOT NULL,
|
|
26170
|
+
updated_at INTEGER NOT NULL,
|
|
26171
|
+
PRIMARY KEY (identity_id, pin_key_b64)
|
|
26172
|
+
);
|
|
26173
|
+
CREATE TABLE IF NOT EXISTS owner_replay (
|
|
26174
|
+
identity_id TEXT NOT NULL,
|
|
26175
|
+
channel_id TEXT NOT NULL,
|
|
26176
|
+
message_id TEXT NOT NULL,
|
|
26177
|
+
group_id TEXT NOT NULL,
|
|
26178
|
+
epoch INTEGER NOT NULL,
|
|
26179
|
+
processed_at INTEGER NOT NULL,
|
|
26180
|
+
PRIMARY KEY (identity_id, channel_id, message_id)
|
|
26181
|
+
);
|
|
26182
|
+
CREATE INDEX IF NOT EXISTS owner_replay_age ON owner_replay (identity_id, channel_id, processed_at);
|
|
26183
|
+
CREATE TABLE IF NOT EXISTS owner_verify_failed (
|
|
26184
|
+
identity_id TEXT NOT NULL,
|
|
26185
|
+
channel_id TEXT NOT NULL,
|
|
26186
|
+
message_id TEXT NOT NULL,
|
|
26187
|
+
failed_at INTEGER NOT NULL,
|
|
26188
|
+
PRIMARY KEY (identity_id, message_id)
|
|
26189
|
+
);
|
|
26190
|
+
CREATE TABLE IF NOT EXISTS owner_replay_closed_epoch (
|
|
26191
|
+
identity_id TEXT NOT NULL,
|
|
26192
|
+
channel_id TEXT NOT NULL,
|
|
26193
|
+
group_id TEXT NOT NULL,
|
|
26194
|
+
epoch INTEGER NOT NULL,
|
|
26195
|
+
closed_at INTEGER NOT NULL,
|
|
26196
|
+
PRIMARY KEY (identity_id, channel_id, group_id, epoch)
|
|
26197
|
+
);
|
|
26198
|
+
`);
|
|
26199
|
+
}
|
|
26121
26200
|
|
|
26122
26201
|
// ../connect-mls-store/dist/errors.js
|
|
26123
26202
|
var MlsStoreCorruptError = class extends Error {
|
|
@@ -26272,7 +26351,16 @@ function itemVecAad(identityId, f, modelId) {
|
|
|
26272
26351
|
return enc4.encode(["agent_memory_item_vec", identityId, f.scopeKind, f.scopeId, f.originId, String(f.chunkIndex), modelId].join(NUL));
|
|
26273
26352
|
}
|
|
26274
26353
|
function encodeItemRecord(r) {
|
|
26275
|
-
return enc4.encode(JSON.stringify({
|
|
26354
|
+
return enc4.encode(JSON.stringify({
|
|
26355
|
+
v: 1,
|
|
26356
|
+
t: r.text,
|
|
26357
|
+
s: r.source,
|
|
26358
|
+
tr: r.trust,
|
|
26359
|
+
...r.sourceRef !== void 0 ? { sr: r.sourceRef } : {},
|
|
26360
|
+
...r.chunks !== void 0 ? { c: r.chunks } : {},
|
|
26361
|
+
...r.role !== void 0 ? { ro: r.role } : {},
|
|
26362
|
+
...r.origin !== void 0 ? { or: r.origin } : {}
|
|
26363
|
+
}));
|
|
26276
26364
|
}
|
|
26277
26365
|
function decodeItemRecord(b) {
|
|
26278
26366
|
let o;
|
|
@@ -26283,7 +26371,7 @@ function decodeItemRecord(b) {
|
|
|
26283
26371
|
}
|
|
26284
26372
|
if (!o || typeof o !== "object" || Array.isArray(o))
|
|
26285
26373
|
return null;
|
|
26286
|
-
const { v, t, s, tr, sr, c } = o;
|
|
26374
|
+
const { v, t, s, tr, sr, c, ro, or } = o;
|
|
26287
26375
|
if (v !== 1 || typeof t !== "string" || typeof s !== "string" || !ITEM_SOURCES.includes(s))
|
|
26288
26376
|
return null;
|
|
26289
26377
|
if (tr !== "owner" && tr !== "reference")
|
|
@@ -26294,7 +26382,19 @@ function decodeItemRecord(b) {
|
|
|
26294
26382
|
return null;
|
|
26295
26383
|
if (c !== void 0 && (typeof c !== "number" || !Number.isSafeInteger(c) || c < 1))
|
|
26296
26384
|
return null;
|
|
26297
|
-
|
|
26385
|
+
if (ro !== void 0 && ro !== "peer" && ro !== "agent")
|
|
26386
|
+
return null;
|
|
26387
|
+
if (or !== void 0 && or !== "fast_path" && or !== "reply" && or !== "untrusted")
|
|
26388
|
+
return null;
|
|
26389
|
+
return {
|
|
26390
|
+
text: t,
|
|
26391
|
+
source: s,
|
|
26392
|
+
trust: tr,
|
|
26393
|
+
...sr !== void 0 ? { sourceRef: sr } : {},
|
|
26394
|
+
...c !== void 0 ? { chunks: c } : {},
|
|
26395
|
+
...ro !== void 0 ? { role: ro } : {},
|
|
26396
|
+
...or !== void 0 ? { origin: or } : {}
|
|
26397
|
+
};
|
|
26298
26398
|
}
|
|
26299
26399
|
var noteSourceRef = (conversationId, noteId) => `agent_memory_note:${conversationId}:${noteId}`;
|
|
26300
26400
|
var recallSourceRef = (conversationId, messageId) => `agent_memory_recall:${conversationId}:${messageId}`;
|
|
@@ -26338,6 +26438,14 @@ function bytesEqual(a, b) {
|
|
|
26338
26438
|
function sameTag(prior, tag) {
|
|
26339
26439
|
return prior !== null && prior.groupId === tag.groupId && prior.epoch === tag.epoch;
|
|
26340
26440
|
}
|
|
26441
|
+
function metaOf(r) {
|
|
26442
|
+
return {
|
|
26443
|
+
...r.message_id !== null ? { messageId: r.message_id } : {},
|
|
26444
|
+
modality: r.modality,
|
|
26445
|
+
...isOrigin(r.origin) ? { origin: r.origin } : {}
|
|
26446
|
+
};
|
|
26447
|
+
}
|
|
26448
|
+
var isOrigin = (v) => v === "fast_path" || v === "reply" || v === "untrusted";
|
|
26341
26449
|
var UTF8 = new TextDecoder();
|
|
26342
26450
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
26343
26451
|
var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
|
|
@@ -26386,6 +26494,8 @@ function cachePreparedStatements(db) {
|
|
|
26386
26494
|
return st;
|
|
26387
26495
|
};
|
|
26388
26496
|
}
|
|
26497
|
+
var OWNER_REPLAY_MAX_PER_CHANNEL = 1e4;
|
|
26498
|
+
var OWNER_REPLAY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26389
26499
|
var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
26390
26500
|
db;
|
|
26391
26501
|
id;
|
|
@@ -26759,7 +26869,13 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
26759
26869
|
* same message (the bridge's ingest stamp). The staged row is never
|
|
26760
26870
|
* rewritten: on a match the FIRST staging stays authoritative.
|
|
26761
26871
|
*/
|
|
26762
|
-
|
|
26872
|
+
/**
|
|
26873
|
+
* Owner-signed tool grants (P-3, plan 1 task 4): when `ownerReplay` is given,
|
|
26874
|
+
* the owner replay entry and the runtime_inbox row are written in ONE
|
|
26875
|
+
* BEGIN IMMEDIATE transaction, so a failed stage records no replay entry and a
|
|
26876
|
+
* recorded replay entry always has its staged message.
|
|
26877
|
+
*/
|
|
26878
|
+
async stageRuntimeInbox(messageId, payload, receivedAt = Date.now(), samePayload = bytesEqual, ownerReplay) {
|
|
26763
26879
|
const prior = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
|
|
26764
26880
|
if (prior) {
|
|
26765
26881
|
const priorPayload = await this.openRuntimeInboxPayload(prior.sealed_message);
|
|
@@ -26769,9 +26885,20 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
26769
26885
|
return false;
|
|
26770
26886
|
}
|
|
26771
26887
|
const sealed = await this.cipher.seal(payload);
|
|
26772
|
-
const info = this.
|
|
26773
|
-
|
|
26774
|
-
|
|
26888
|
+
const info = this.immediate(() => {
|
|
26889
|
+
if (ownerReplay) {
|
|
26890
|
+
this.db.prepare(`INSERT OR IGNORE INTO owner_replay
|
|
26891
|
+
(identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
26892
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, ownerReplay.channelId, messageId, ownerReplay.groupId, ownerReplay.epoch, ownerReplay.processedAt);
|
|
26893
|
+
}
|
|
26894
|
+
if (this.failNextRuntimeInboxInsert) {
|
|
26895
|
+
this.failNextRuntimeInboxInsert = false;
|
|
26896
|
+
throw new Error("runtime_inbox insert failed (test)");
|
|
26897
|
+
}
|
|
26898
|
+
return this.db.prepare(`INSERT OR IGNORE INTO runtime_inbox
|
|
26899
|
+
(identity_id, message_id, sealed_message, received_at)
|
|
26900
|
+
VALUES (?, ?, ?, ?)`).run(this.id, messageId, sealed, receivedAt);
|
|
26901
|
+
});
|
|
26775
26902
|
if (info.changes === 0) {
|
|
26776
26903
|
const raced = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
|
|
26777
26904
|
const racedPayload = await this.openRuntimeInboxPayload(raced.sealed_message);
|
|
@@ -27054,10 +27181,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27054
27181
|
return { seq: 0, deduped: false, dropped: true };
|
|
27055
27182
|
}
|
|
27056
27183
|
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)
|
|
27184
|
+
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at, origin)
|
|
27058
27185
|
VALUES (?, ?,
|
|
27059
27186
|
(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);
|
|
27187
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?)`).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
27188
|
if (info.changes === 0) {
|
|
27062
27189
|
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
27190
|
return { seq: row2.seq, deduped: true };
|
|
@@ -27081,6 +27208,29 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27081
27208
|
facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
|
|
27082
27209
|
return { recent, facts };
|
|
27083
27210
|
}
|
|
27211
|
+
/** Owner grants plan 4 Task 16: message id, modality and origin of the given lines (clear columns only, no text). */
|
|
27212
|
+
memoryLineMeta(conversationId, seqs) {
|
|
27213
|
+
const out = /* @__PURE__ */ new Map();
|
|
27214
|
+
const stmt = this.db.prepare("SELECT message_id, modality, origin FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq=?");
|
|
27215
|
+
for (const seq of seqs) {
|
|
27216
|
+
const r = stmt.get(this.id, conversationId, seq);
|
|
27217
|
+
if (r)
|
|
27218
|
+
out.set(seq, metaOf(r));
|
|
27219
|
+
}
|
|
27220
|
+
return out;
|
|
27221
|
+
}
|
|
27222
|
+
/** Owner grants plan 4 Task 16 (audit r2 M-1): every peer row's message id in the conversation, not the 50-row window. */
|
|
27223
|
+
memoryPeerMessageIds(conversationId) {
|
|
27224
|
+
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);
|
|
27225
|
+
}
|
|
27226
|
+
/** Owner grants plan 4 Task 16 (OQ-9): the history row an imported_history item came from, or null when it is gone. */
|
|
27227
|
+
memoryRowForTrace(conversationId, at) {
|
|
27228
|
+
const cols = "role, peer_id, message_id, modality, origin, recorded_at";
|
|
27229
|
+
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;
|
|
27230
|
+
if (!r)
|
|
27231
|
+
return null;
|
|
27232
|
+
return { role: r.role === "agent" ? "agent" : "peer", ts: Number(r.recorded_at), ...r.peer_id ? { peerId: r.peer_id } : {}, ...metaOf(r) };
|
|
27233
|
+
}
|
|
27084
27234
|
async recordMemoryFact(conversationId, fact) {
|
|
27085
27235
|
const now = this.memoryNow();
|
|
27086
27236
|
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
@@ -28108,7 +28258,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28108
28258
|
}
|
|
28109
28259
|
/** §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
28260
|
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>?
|
|
28261
|
+
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
28262
|
ORDER BY seq ASC LIMIT ?`).all(this.id, conversationId, sinceMs, afterSeq, limit);
|
|
28113
28263
|
const rows = [];
|
|
28114
28264
|
let unreadable = 0;
|
|
@@ -28117,7 +28267,15 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28117
28267
|
await yieldToLoop();
|
|
28118
28268
|
try {
|
|
28119
28269
|
const text = UTF8.decode(await this.cipher.open(r.sealed_text));
|
|
28120
|
-
rows.push({
|
|
28270
|
+
rows.push({
|
|
28271
|
+
seq: Number(r.seq),
|
|
28272
|
+
messageId: r.message_id ?? null,
|
|
28273
|
+
role: r.role === "agent" ? "agent" : "peer",
|
|
28274
|
+
text,
|
|
28275
|
+
recordedAt: Number(r.recorded_at),
|
|
28276
|
+
modality: r.modality,
|
|
28277
|
+
...isOrigin(r.origin) ? { origin: r.origin } : {}
|
|
28278
|
+
});
|
|
28121
28279
|
} catch {
|
|
28122
28280
|
unreadable++;
|
|
28123
28281
|
}
|
|
@@ -28519,6 +28677,156 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28519
28677
|
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
28678
|
return Number(res.changes) > 0;
|
|
28521
28679
|
}
|
|
28680
|
+
// ---------------------------------------------------------------------------
|
|
28681
|
+
// Owner-signed tool grants, step 1 (plan 1 task 4; spec §7.3, §7.11; D-2).
|
|
28682
|
+
/** The last good manifest wrapper stored for this pinned public key, or null. */
|
|
28683
|
+
async loadToolGrantManifest(pinKeyB64) {
|
|
28684
|
+
const row = this.db.prepare("SELECT counter, sealed_manifest FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
|
|
28685
|
+
if (!row)
|
|
28686
|
+
return null;
|
|
28687
|
+
return { counter: Number(row.counter), wrapper: await this.unsealJson(row.sealed_manifest) };
|
|
28688
|
+
}
|
|
28689
|
+
/** Stores a verified wrapper. The counter can never fall or repeat, whatever the
|
|
28690
|
+
* caller does: the upsert's WHERE refuses it and this throws. */
|
|
28691
|
+
async saveToolGrantManifest(pinKeyB64, counter, wrapper) {
|
|
28692
|
+
const sealed = await this.sealJson(wrapper);
|
|
28693
|
+
const info = this.db.prepare(`INSERT INTO tool_grant_manifest (identity_id, pin_key_b64, counter, sealed_manifest, updated_at)
|
|
28694
|
+
VALUES (?, ?, ?, ?, ?)
|
|
28695
|
+
ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
|
|
28696
|
+
counter = excluded.counter, sealed_manifest = excluded.sealed_manifest, updated_at = excluded.updated_at
|
|
28697
|
+
WHERE excluded.counter > tool_grant_manifest.counter`).run(this.id, pinKeyB64, counter, sealed, Date.now());
|
|
28698
|
+
if (Number(info.changes) === 0)
|
|
28699
|
+
throw new Error("tool grant counter not increasing");
|
|
28700
|
+
}
|
|
28701
|
+
/** Counter rule §6.6: state is per pinned key; a new key starts a new counter. */
|
|
28702
|
+
dropToolGrantManifestsExcept(pinKeyB64) {
|
|
28703
|
+
return Number(this.db.prepare("DELETE FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
28704
|
+
}
|
|
28705
|
+
// Owner-signed tool grants plan 4, Task 12 (spec §7.5): the room web_search backstop, per room per UTC day.
|
|
28706
|
+
/** A row of another day reads as zero. */
|
|
28707
|
+
roomSearchCapState(channelId, utcDay) {
|
|
28708
|
+
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);
|
|
28709
|
+
if (!row)
|
|
28710
|
+
return { grantedTurns: 0, reachedBy: null, noticePosted: false };
|
|
28711
|
+
const by = row.reached_by === "bridge" || row.reached_by === "runtime" ? row.reached_by : null;
|
|
28712
|
+
return { grantedTurns: Number(row.granted_turns), reachedBy: by, noticePosted: Number(row.notice_posted) === 1 };
|
|
28713
|
+
}
|
|
28714
|
+
/** One statement: a row of an earlier day is reset, then +1. Returns the new count. */
|
|
28715
|
+
countRoomSearchGrant(channelId, utcDay) {
|
|
28716
|
+
const row = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
|
|
28717
|
+
VALUES (?, ?, ?, 1, NULL, 0)
|
|
28718
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET
|
|
28719
|
+
granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns + 1 ELSE 1 END,
|
|
28720
|
+
reached_by = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.reached_by ELSE NULL END,
|
|
28721
|
+
notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
|
|
28722
|
+
utc_day = excluded.utc_day
|
|
28723
|
+
RETURNING granted_turns`).get(this.id, channelId, utcDay);
|
|
28724
|
+
return Number(row.granted_turns);
|
|
28725
|
+
}
|
|
28726
|
+
/** True when this call set the flag (the first time today). */
|
|
28727
|
+
markRoomSearchCapReached(channelId, utcDay, by) {
|
|
28728
|
+
const info = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
|
|
28729
|
+
VALUES (?, ?, ?, 0, ?, 0)
|
|
28730
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET
|
|
28731
|
+
granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns ELSE 0 END,
|
|
28732
|
+
notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
|
|
28733
|
+
reached_by = excluded.reached_by,
|
|
28734
|
+
utc_day = excluded.utc_day
|
|
28735
|
+
WHERE room_search_cap.utc_day <> excluded.utc_day OR room_search_cap.reached_by IS NULL`).run(this.id, channelId, utcDay, by);
|
|
28736
|
+
return Number(info.changes) > 0;
|
|
28737
|
+
}
|
|
28738
|
+
markRoomSearchCapNoticePosted(channelId, utcDay) {
|
|
28739
|
+
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);
|
|
28740
|
+
}
|
|
28741
|
+
/** Deletes rows of earlier days; called at start(). */
|
|
28742
|
+
pruneRoomSearchCap(utcDay) {
|
|
28743
|
+
return Number(this.db.prepare("DELETE FROM room_search_cap WHERE identity_id=? AND utc_day < ?").run(this.id, utcDay).changes);
|
|
28744
|
+
}
|
|
28745
|
+
// Owner-signed tool grants plan 4, Task 11 (rulings item 3): the last applied good search block.
|
|
28746
|
+
/** The applied record for this pinned key, or null. `search: null` means "applied with no search block". */
|
|
28747
|
+
async loadToolGrantApplied(pinKeyB64) {
|
|
28748
|
+
const row = this.db.prepare("SELECT sealed_applied FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
|
|
28749
|
+
if (!row)
|
|
28750
|
+
return null;
|
|
28751
|
+
return await this.unsealJson(row.sealed_applied);
|
|
28752
|
+
}
|
|
28753
|
+
async saveToolGrantApplied(pinKeyB64, applied) {
|
|
28754
|
+
const sealed = await this.sealJson({ counter: applied.counter, search: applied.search });
|
|
28755
|
+
this.db.prepare(`INSERT INTO tool_grant_applied (identity_id, pin_key_b64, sealed_applied, updated_at)
|
|
28756
|
+
VALUES (?, ?, ?, ?)
|
|
28757
|
+
ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
|
|
28758
|
+
sealed_applied = excluded.sealed_applied, updated_at = excluded.updated_at`).run(this.id, pinKeyB64, sealed, Date.now());
|
|
28759
|
+
}
|
|
28760
|
+
/** Counter rule §6.6: a new pinned key drops the old key's applied record. */
|
|
28761
|
+
dropToolGrantAppliedExcept(pinKeyB64) {
|
|
28762
|
+
return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
28763
|
+
}
|
|
28764
|
+
hasOwnerMessage(channelId, messageId) {
|
|
28765
|
+
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;
|
|
28766
|
+
}
|
|
28767
|
+
isOwnerEpochClosed(channelId, groupId, epoch) {
|
|
28768
|
+
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;
|
|
28769
|
+
}
|
|
28770
|
+
recordOwnerVerifyFailure(channelId, messageId, failedAt = Date.now()) {
|
|
28771
|
+
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);
|
|
28772
|
+
}
|
|
28773
|
+
isOwnerVerifyFailed(messageId) {
|
|
28774
|
+
return this.db.prepare("SELECT 1 FROM owner_verify_failed WHERE identity_id=? AND message_id=?").get(this.id, messageId) !== void 0;
|
|
28775
|
+
}
|
|
28776
|
+
listOwnerReplayChannels() {
|
|
28777
|
+
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);
|
|
28778
|
+
}
|
|
28779
|
+
/**
|
|
28780
|
+
* Decision D-2. Per channel with a known current (group, epoch): rows of the
|
|
28781
|
+
* current pair are never pruned; among the others, drop rows older than
|
|
28782
|
+
* `maxAgeMs`, then every row past the newest `maxPerChannel`, oldest first.
|
|
28783
|
+
* Every pair that lost a row is recorded closed. A channel with no current
|
|
28784
|
+
* position is skipped this pass. Old `owner_verify_failed` rows go too.
|
|
28785
|
+
* Returns the number of owner_replay rows removed.
|
|
28786
|
+
*/
|
|
28787
|
+
pruneOwnerReplay(nowMs, currentOf, opts = {}) {
|
|
28788
|
+
const maxPerChannel = opts.maxPerChannel ?? OWNER_REPLAY_MAX_PER_CHANNEL;
|
|
28789
|
+
const maxAgeMs = opts.maxAgeMs ?? OWNER_REPLAY_MAX_AGE_MS;
|
|
28790
|
+
const cutoff = nowMs - maxAgeMs;
|
|
28791
|
+
return this.immediate(() => {
|
|
28792
|
+
let removed = 0;
|
|
28793
|
+
for (const channelId of this.listOwnerReplayChannels()) {
|
|
28794
|
+
const cur = currentOf.get(channelId);
|
|
28795
|
+
if (!cur)
|
|
28796
|
+
continue;
|
|
28797
|
+
const others = this.db.prepare(`SELECT rowid AS rid, group_id, epoch, processed_at FROM owner_replay
|
|
28798
|
+
WHERE identity_id=? AND channel_id=? AND NOT (group_id=? AND epoch=?)
|
|
28799
|
+
ORDER BY processed_at DESC, rowid DESC`).all(this.id, channelId, cur.groupId, cur.epoch);
|
|
28800
|
+
const young = others.filter((r) => Number(r.processed_at) >= cutoff);
|
|
28801
|
+
const victims = [...others.filter((r) => Number(r.processed_at) < cutoff), ...young.slice(maxPerChannel)];
|
|
28802
|
+
const closed = /* @__PURE__ */ new Map();
|
|
28803
|
+
const del = this.db.prepare("DELETE FROM owner_replay WHERE rowid=?");
|
|
28804
|
+
for (const v of victims) {
|
|
28805
|
+
removed += Number(del.run(v.rid).changes);
|
|
28806
|
+
closed.set(`${v.group_id}\0${v.epoch}`, { groupId: v.group_id, epoch: Number(v.epoch) });
|
|
28807
|
+
}
|
|
28808
|
+
const close = this.db.prepare(`INSERT OR IGNORE INTO owner_replay_closed_epoch (identity_id, channel_id, group_id, epoch, closed_at)
|
|
28809
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
28810
|
+
for (const p of closed.values()) {
|
|
28811
|
+
if (p.groupId === cur.groupId && p.epoch === cur.epoch)
|
|
28812
|
+
continue;
|
|
28813
|
+
close.run(this.id, channelId, p.groupId, p.epoch, nowMs);
|
|
28814
|
+
}
|
|
28815
|
+
}
|
|
28816
|
+
this.db.prepare("DELETE FROM owner_verify_failed WHERE identity_id=? AND failed_at < ?").run(this.id, cutoff);
|
|
28817
|
+
return removed;
|
|
28818
|
+
});
|
|
28819
|
+
}
|
|
28820
|
+
failNextRuntimeInboxInsert = false;
|
|
28821
|
+
/** @internal test-only: the next runtime_inbox INSERT inside the staging transaction throws once. */
|
|
28822
|
+
_failNextRuntimeInboxInsertForTest() {
|
|
28823
|
+
this.failNextRuntimeInboxInsert = true;
|
|
28824
|
+
}
|
|
28825
|
+
/** @internal test-only */
|
|
28826
|
+
_insertOwnerReplayForTest(channelId, messageId, groupId, epoch, processedAt) {
|
|
28827
|
+
this.db.prepare(`INSERT INTO owner_replay (identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
28828
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
|
|
28829
|
+
}
|
|
28522
28830
|
/** @internal test-only */
|
|
28523
28831
|
_rawMainForTest(identityId, channelId) {
|
|
28524
28832
|
const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
|
|
@@ -28671,8 +28979,8 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
28671
28979
|
}
|
|
28672
28980
|
|
|
28673
28981
|
// src/bridge-agent.ts
|
|
28674
|
-
import { promises as fsp, statSync as statSync4 } from "node:fs";
|
|
28675
|
-
import { join as
|
|
28982
|
+
import { existsSync as existsSync5, promises as fsp, statSync as statSync4 } from "node:fs";
|
|
28983
|
+
import { join as join17, dirname as dirname5 } from "node:path";
|
|
28676
28984
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28677
28985
|
|
|
28678
28986
|
// src/latency.ts
|
|
@@ -29263,8 +29571,8 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
|
|
|
29263
29571
|
async function materializeAttachment(body, ctx, deps) {
|
|
29264
29572
|
await deps.ready();
|
|
29265
29573
|
const ciphertext = await deps.downloadBlob(body.blobId);
|
|
29266
|
-
const
|
|
29267
|
-
if (
|
|
29574
|
+
const sha25612 = await deps.computeFileDigest(ciphertext);
|
|
29575
|
+
if (sha25612 !== body.digest) {
|
|
29268
29576
|
throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
|
|
29269
29577
|
}
|
|
29270
29578
|
const plain = await deps.decryptFile(
|
|
@@ -29277,7 +29585,7 @@ async function materializeAttachment(body, ctx, deps) {
|
|
|
29277
29585
|
const path = `${dir}/${ctx.messageId}-${safeName}`;
|
|
29278
29586
|
await deps.mkdirp(dir);
|
|
29279
29587
|
await deps.writeFile(path, plain);
|
|
29280
|
-
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256:
|
|
29588
|
+
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha25612 };
|
|
29281
29589
|
}
|
|
29282
29590
|
|
|
29283
29591
|
// src/sodium-ready.ts
|
|
@@ -29866,11 +30174,11 @@ function quoteFromMessage(note, record) {
|
|
|
29866
30174
|
}
|
|
29867
30175
|
function renderNotes(notes) {
|
|
29868
30176
|
if (notes.length === 0) return void 0;
|
|
29869
|
-
const
|
|
30177
|
+
const lines2 = notes.map((n) => {
|
|
29870
30178
|
const one = oneLine(n.text);
|
|
29871
30179
|
return `- [n${n.id}] ${/^[#\-\[]/.test(one) ? `\\${one}` : one}`;
|
|
29872
30180
|
});
|
|
29873
|
-
return [NOTES_HEADING, ...
|
|
30181
|
+
return [NOTES_HEADING, ...lines2].join("\n");
|
|
29874
30182
|
}
|
|
29875
30183
|
|
|
29876
30184
|
// src/memory/secret-scan.ts
|
|
@@ -30619,7 +30927,7 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
|
|
|
30619
30927
|
|
|
30620
30928
|
// src/memory/embedder/manifest.ts
|
|
30621
30929
|
var MODEL_ORIGIN = "https://downloads.securecoms.ai";
|
|
30622
|
-
var file = (modelId, name,
|
|
30930
|
+
var file = (modelId, name, sha25612, size) => Object.freeze({ name, urlPath: `/models/embeddings/${modelId}/${sha25612}/${name}`, sha256: sha25612, size });
|
|
30623
30931
|
var MODEL_ID = "minilm-l6-v2-int8";
|
|
30624
30932
|
var EMBEDDING_MANIFEST = Object.freeze({
|
|
30625
30933
|
modelId: MODEL_ID,
|
|
@@ -30963,8 +31271,8 @@ import { join as join11, win32 } from "node:path";
|
|
|
30963
31271
|
var WINDOWS_SEMANTIC_ENABLED = false;
|
|
30964
31272
|
var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
|
|
30965
31273
|
function parseSaveSddl(saveFile) {
|
|
30966
|
-
const
|
|
30967
|
-
return (
|
|
31274
|
+
const lines2 = saveFile.toString("utf16le").replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
31275
|
+
return (lines2[1] ?? "").trim();
|
|
30968
31276
|
}
|
|
30969
31277
|
function aclAllows(sddl, ownerSid) {
|
|
30970
31278
|
const allowed = /* @__PURE__ */ new Set([ownerSid, "SY", "BA"]);
|
|
@@ -32359,7 +32667,15 @@ var ItemBook = class {
|
|
|
32359
32667
|
originAt: r.originAt,
|
|
32360
32668
|
storedAt
|
|
32361
32669
|
});
|
|
32362
|
-
const rec = (text, chunks) => ({
|
|
32670
|
+
const rec = (text, chunks) => ({
|
|
32671
|
+
text,
|
|
32672
|
+
source: r.source,
|
|
32673
|
+
trust: r.trust,
|
|
32674
|
+
...r.sourceRef !== void 0 ? { sourceRef: r.sourceRef } : {},
|
|
32675
|
+
...chunks ? { chunks } : {},
|
|
32676
|
+
...r.role !== void 0 ? { role: r.role } : {},
|
|
32677
|
+
...r.origin !== void 0 ? { origin: r.origin } : {}
|
|
32678
|
+
});
|
|
32363
32679
|
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
32680
|
const inlineOk = r.pendingOnly !== true && !this.d.paused() && this.d.embedOn() && this.d.embedder.state().state === "ready";
|
|
32365
32681
|
for (const [i, text] of texts.entries()) {
|
|
@@ -32638,7 +32954,7 @@ var UnifiedReadBook = class {
|
|
|
32638
32954
|
this.entries.set(p.messageId, { channelId: p.channelId, scopeId: p.scopeId, capture, pins: pins?.pins ?? [], pinnedMore: pins?.more ?? 0, items });
|
|
32639
32955
|
}
|
|
32640
32956
|
/** ws flush (synchronous): the entry's own channel and owner scope, its capture still current (H-4, U11). */
|
|
32641
|
-
deliverable(messageId, channelId, scopeIdNow) {
|
|
32957
|
+
deliverable(messageId, channelId, scopeIdNow, keep = () => true) {
|
|
32642
32958
|
const e = this.entries.get(messageId);
|
|
32643
32959
|
if (!e || e.channelId !== channelId || e.scopeId !== scopeIdNow) return void 0;
|
|
32644
32960
|
if (!same(e.capture, this.d.store.itemCapture(channelId))) {
|
|
@@ -32647,8 +32963,9 @@ var UnifiedReadBook = class {
|
|
|
32647
32963
|
}
|
|
32648
32964
|
const at = (i) => ({ id: i.itemId, source: i.source, at: i.originAt, text: clampText(i.text) });
|
|
32649
32965
|
const all = [
|
|
32650
|
-
|
|
32651
|
-
...e.
|
|
32966
|
+
// Owner grants plan 4 Task 16: `keep` is the voice fence on a pinned agent (never a SQL read here; the bridge supplies it).
|
|
32967
|
+
...e.pins.filter(keep).map((i) => ({ block: "pinned", ...at(i) })),
|
|
32968
|
+
...e.items.filter(keep).map((i) => ({ block: i.trust === "owner" ? "owner" : "reference", ...at(i) }))
|
|
32652
32969
|
];
|
|
32653
32970
|
const items = all.slice(0, FRAME_ITEMS_MAX);
|
|
32654
32971
|
const pinsCut = Math.max(0, e.pins.length - FRAME_ITEMS_MAX);
|
|
@@ -32693,6 +33010,56 @@ var UnifiedReadBook = class {
|
|
|
32693
33010
|
}
|
|
32694
33011
|
};
|
|
32695
33012
|
|
|
33013
|
+
// src/grants/voice-fence.ts
|
|
33014
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
33015
|
+
var FAST_ID_EPOCH_MS = 17887392e5;
|
|
33016
|
+
function legacyFastReplyId(messageId) {
|
|
33017
|
+
const h = createHash4("sha256").update(`fast:${messageId}`).digest();
|
|
33018
|
+
h[6] = h[6] & 15 | 64;
|
|
33019
|
+
h[8] = h[8] & 63 | 128;
|
|
33020
|
+
const x = h.subarray(0, 16).toString("hex");
|
|
33021
|
+
return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
|
|
33022
|
+
}
|
|
33023
|
+
function classifyAgentLine(line, ctx) {
|
|
33024
|
+
if (line.origin === "fast_path" || line.origin === "reply" || line.origin === "untrusted") return line.origin;
|
|
33025
|
+
if (line.modality === "voice") return "fast_path";
|
|
33026
|
+
if (line.messageId !== void 0 && ctx.fastIds.has(line.messageId)) return "fast_path";
|
|
33027
|
+
if (line.ts !== void 0 && line.ts < FAST_ID_EPOCH_MS) return "fast_path";
|
|
33028
|
+
if (line.modality === "text") return "reply";
|
|
33029
|
+
return "fast_path";
|
|
33030
|
+
}
|
|
33031
|
+
function isRelayClassPeer(line, ownerId) {
|
|
33032
|
+
return line.modality === "voice" || line.peerId !== ownerId;
|
|
33033
|
+
}
|
|
33034
|
+
function parseHistoryRef(ref) {
|
|
33035
|
+
const m = /^agent_memory:(.+):(\d+)$/.exec(ref);
|
|
33036
|
+
return m ? { conversationId: m[1], seq: Number(m[2]) } : null;
|
|
33037
|
+
}
|
|
33038
|
+
function classifyUnifiedItem(item, trace, ctx, strict, keepUntrusted = false) {
|
|
33039
|
+
if (strict?.turnChannelId !== void 0 && item.channelId !== strict.turnChannelId) {
|
|
33040
|
+
if (item.source === "owner_typed") return "keep";
|
|
33041
|
+
if (item.source !== "imported_history") return "exclude";
|
|
33042
|
+
const row2 = traceItem(item, trace);
|
|
33043
|
+
return row2 !== null && row2.role === "peer" && !isRelayClassPeer(row2, strict.ownerId) ? "keep" : "exclude";
|
|
33044
|
+
}
|
|
33045
|
+
if (strict && item.source === "owner_voice") return "exclude";
|
|
33046
|
+
if (item.role === "peer" && !(strict && item.source === "imported_history")) return "keep";
|
|
33047
|
+
if (item.role !== "peer" && item.origin !== void 0) return item.origin === "reply" || keepUntrusted && item.origin === "untrusted" ? "keep" : "exclude";
|
|
33048
|
+
if (item.source === "agent_reply") return "keep";
|
|
33049
|
+
if (item.source !== "imported_history") return "keep";
|
|
33050
|
+
const row = traceItem(item, trace);
|
|
33051
|
+
if (row === null) return "exclude";
|
|
33052
|
+
if (row.role === "peer") return strict && isRelayClassPeer(row, strict.ownerId) ? "exclude" : "keep";
|
|
33053
|
+
const origin = classifyAgentLine(row, ctx);
|
|
33054
|
+
return origin === "reply" || keepUntrusted && origin === "untrusted" ? "keep" : "exclude";
|
|
33055
|
+
}
|
|
33056
|
+
function traceItem(item, trace) {
|
|
33057
|
+
const ref = item.sourceRef !== void 0 ? parseHistoryRef(item.sourceRef) : null;
|
|
33058
|
+
if (ref !== null) return trace(ref.conversationId, { seq: ref.seq });
|
|
33059
|
+
const seqForm = /^seq:(\d+)$/.exec(item.originId);
|
|
33060
|
+
return seqForm ? trace(item.channelId, { seq: Number(seqForm[1]) }) : trace(item.channelId, { messageId: item.originId });
|
|
33061
|
+
}
|
|
33062
|
+
|
|
32696
33063
|
// src/memory/unified/migrate.ts
|
|
32697
33064
|
var OLD_TABLES_PRESENT = true;
|
|
32698
33065
|
var MIGRATION_BATCH = 100;
|
|
@@ -32845,7 +33212,10 @@ async function runMigration(d, o = {}) {
|
|
|
32845
33212
|
text: r.text,
|
|
32846
33213
|
sourceRef: historySourceRef(c, r.seq),
|
|
32847
33214
|
pendingOnly: true,
|
|
32848
|
-
expect
|
|
33215
|
+
expect,
|
|
33216
|
+
// Owner grants plan 4 Task 16: the row's role and, for an agent row, its classified origin.
|
|
33217
|
+
role: r.role,
|
|
33218
|
+
...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
33219
|
}),
|
|
32850
33220
|
(r) => r.recordedAt,
|
|
32851
33221
|
t
|
|
@@ -32903,9 +33273,9 @@ async function recordInboundMemory(sink, channelId, senderId, text, messageId, m
|
|
|
32903
33273
|
} catch {
|
|
32904
33274
|
}
|
|
32905
33275
|
}
|
|
32906
|
-
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch) {
|
|
33276
|
+
async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch, origin) {
|
|
32907
33277
|
try {
|
|
32908
|
-
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch } });
|
|
33278
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch }, ...origin ? { origin } : {} });
|
|
32909
33279
|
} catch {
|
|
32910
33280
|
}
|
|
32911
33281
|
}
|
|
@@ -33261,10 +33631,10 @@ function splitFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
33261
33631
|
return { reply: normalizeFastReply(raw.replace(DEFER_RE, " "), maxChars, maxSentences), defer };
|
|
33262
33632
|
}
|
|
33263
33633
|
function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
33264
|
-
const
|
|
33634
|
+
const lines2 = raw.split(/\r?\n+/).map(
|
|
33265
33635
|
(l) => l.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").replace(/^\s*#{1,6}\s+/, "").replace(/[*_`]+/g, "").trim()
|
|
33266
33636
|
).filter(Boolean);
|
|
33267
|
-
const sentences =
|
|
33637
|
+
const sentences = lines2.flatMap((l) => l.split(SENTENCE_END)).map((s) => s.trim()).filter(Boolean);
|
|
33268
33638
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
33269
33639
|
}
|
|
33270
33640
|
|
|
@@ -33476,18 +33846,18 @@ function parseSummarizeBody(parsed, selfIdentityId, log = () => {
|
|
|
33476
33846
|
function transcriptToPrompt(transcript) {
|
|
33477
33847
|
const names = /* @__PURE__ */ new Map();
|
|
33478
33848
|
for (const p of transcript.participants ?? []) names.set(p.identity_id, p.display_name);
|
|
33479
|
-
const
|
|
33849
|
+
const lines2 = transcript.turns.map(
|
|
33480
33850
|
(t) => `${names.get(t.speaker_identity_id) ?? t.speaker_identity_id}: ${t.text}`
|
|
33481
33851
|
);
|
|
33482
|
-
return `${SUMMARY_PROMPT}${
|
|
33852
|
+
return `${SUMMARY_PROMPT}${lines2.join("\n")}
|
|
33483
33853
|
`;
|
|
33484
33854
|
}
|
|
33485
33855
|
function parseSummaryOutput(text) {
|
|
33486
|
-
const
|
|
33487
|
-
const idx =
|
|
33856
|
+
const lines2 = text.split("\n");
|
|
33857
|
+
const idx = lines2.findIndex((line) => /^\s*Action items:\s*$/i.test(line));
|
|
33488
33858
|
if (idx < 0) return { paragraph: text.trim(), actionItems: [] };
|
|
33489
|
-
const paragraph =
|
|
33490
|
-
const actionItems =
|
|
33859
|
+
const paragraph = lines2.slice(0, idx).join("\n").trim();
|
|
33860
|
+
const actionItems = lines2.slice(idx + 1).map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").trim()).filter((line) => line.length > 0);
|
|
33491
33861
|
return { paragraph, actionItems };
|
|
33492
33862
|
}
|
|
33493
33863
|
function buildSummaryBody(request, out, now) {
|
|
@@ -33527,7 +33897,7 @@ async function runSummarize(opts) {
|
|
|
33527
33897
|
}
|
|
33528
33898
|
|
|
33529
33899
|
// src/service/runtime-config-dir.ts
|
|
33530
|
-
import { createHash as
|
|
33900
|
+
import { createHash as createHash5, randomUUID } from "node:crypto";
|
|
33531
33901
|
import {
|
|
33532
33902
|
chmodSync as chmodSync4,
|
|
33533
33903
|
existsSync as existsSync4,
|
|
@@ -33562,7 +33932,24 @@ function assertManagedName(name) {
|
|
|
33562
33932
|
}
|
|
33563
33933
|
function isLocalOnlyName(name) {
|
|
33564
33934
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
33565
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets";
|
|
33935
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
|
|
33936
|
+
}
|
|
33937
|
+
var MANIFEST_POLICY_FILE = "manifest-policy.json";
|
|
33938
|
+
function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
|
|
33939
|
+
try {
|
|
33940
|
+
deps.unlink(join15(runtimeConfigDir(home, identityId), "llm-source.json"));
|
|
33941
|
+
} catch (e) {
|
|
33942
|
+
if (e?.code !== "ENOENT") throw e;
|
|
33943
|
+
}
|
|
33944
|
+
}
|
|
33945
|
+
function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
|
|
33946
|
+
const parent3 = join15(home, ".securecoms", "runtime-config");
|
|
33947
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
33948
|
+
for (const d of [join15(home, ".securecoms"), parent3, dir]) {
|
|
33949
|
+
deps.mkdir(d, 448);
|
|
33950
|
+
deps.chmod(d, 448);
|
|
33951
|
+
}
|
|
33952
|
+
atomicWrite(join15(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
|
|
33566
33953
|
}
|
|
33567
33954
|
function atomicWrite(path, body, deps) {
|
|
33568
33955
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -33618,7 +34005,7 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
|
33618
34005
|
});
|
|
33619
34006
|
}
|
|
33620
34007
|
function lineHash(line) {
|
|
33621
|
-
return
|
|
34008
|
+
return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
|
|
33622
34009
|
}
|
|
33623
34010
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
33624
34011
|
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
@@ -33647,7 +34034,7 @@ var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
|
33647
34034
|
var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
|
|
33648
34035
|
var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
|
|
33649
34036
|
function legacySourceId(source, bytes) {
|
|
33650
|
-
return
|
|
34037
|
+
return createHash5("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
|
|
33651
34038
|
}
|
|
33652
34039
|
function localLegacySources(doc) {
|
|
33653
34040
|
if (!Array.isArray(doc.legacy_memory)) return [];
|
|
@@ -33819,6 +34206,568 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
33819
34206
|
return removed;
|
|
33820
34207
|
}
|
|
33821
34208
|
|
|
34209
|
+
// src/grants/binding.ts
|
|
34210
|
+
import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
34211
|
+
|
|
34212
|
+
// src/grants/manifest.ts
|
|
34213
|
+
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
34214
|
+
|
|
34215
|
+
// src/grants/pin.ts
|
|
34216
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
34217
|
+
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
34218
|
+
import { dirname as dirname4, join as join16 } from "node:path";
|
|
34219
|
+
var PIN_FILE_NAME = "owner-signing-key.json";
|
|
34220
|
+
var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
34221
|
+
var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
|
|
34222
|
+
function canonicalBase64Bytes(v, len) {
|
|
34223
|
+
if (typeof v !== "string" || v.length === 0) return null;
|
|
34224
|
+
const buf = Buffer.from(v, "base64");
|
|
34225
|
+
if (buf.length !== len || buf.toString("base64") !== v) return null;
|
|
34226
|
+
return new Uint8Array(buf);
|
|
34227
|
+
}
|
|
34228
|
+
function parsePinObject(v) {
|
|
34229
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return { ok: false, state: "malformed" };
|
|
34230
|
+
const keys = Object.keys(v).sort();
|
|
34231
|
+
if (keys.length !== PIN_KEYS.length || keys.some((k, i) => k !== PIN_KEYS[i])) return { ok: false, state: "malformed" };
|
|
34232
|
+
const o = v;
|
|
34233
|
+
if (typeof o.key_algo !== "string") return { ok: false, state: "malformed" };
|
|
34234
|
+
if (o.key_algo !== "ed25519") return { ok: false, state: "unsupported_algo" };
|
|
34235
|
+
if (typeof o.owner_identity_id !== "string" || !UUID_LOWER.test(o.owner_identity_id)) return { ok: false, state: "malformed" };
|
|
34236
|
+
const publicKey = canonicalBase64Bytes(o.public_key, 32);
|
|
34237
|
+
if (!publicKey) return { ok: false, state: "malformed" };
|
|
34238
|
+
try {
|
|
34239
|
+
if (ed255194.ExtendedPoint.fromHex(publicKey).isSmallOrder()) return { ok: false, state: "malformed" };
|
|
34240
|
+
} catch {
|
|
34241
|
+
return { ok: false, state: "malformed" };
|
|
34242
|
+
}
|
|
34243
|
+
return {
|
|
34244
|
+
ok: true,
|
|
34245
|
+
pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
|
|
34246
|
+
};
|
|
34247
|
+
}
|
|
34248
|
+
var NODE_FS = { lstat: (p) => lstatSync7(p), readFile: (p) => readFileSync9(p, "utf8") };
|
|
34249
|
+
function fromValue(v) {
|
|
34250
|
+
const r = parsePinObject(v);
|
|
34251
|
+
return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
|
|
34252
|
+
}
|
|
34253
|
+
function readPinFile(path, fs) {
|
|
34254
|
+
let st;
|
|
34255
|
+
try {
|
|
34256
|
+
st = fs.lstat(path);
|
|
34257
|
+
} catch (e) {
|
|
34258
|
+
if (e?.code === "ENOENT") return { kind: "absent" };
|
|
34259
|
+
return { kind: "bad", state: "unreadable" };
|
|
34260
|
+
}
|
|
34261
|
+
if (st.isSymbolicLink()) return { kind: "bad", state: "symlink_refused" };
|
|
34262
|
+
let text;
|
|
34263
|
+
try {
|
|
34264
|
+
text = fs.readFile(path);
|
|
34265
|
+
} catch {
|
|
34266
|
+
return { kind: "bad", state: "unreadable" };
|
|
34267
|
+
}
|
|
34268
|
+
if (st.mode & 63) writeServiceLog2("warn", "config.pin_permissions_unsafe");
|
|
34269
|
+
let json2;
|
|
34270
|
+
try {
|
|
34271
|
+
json2 = JSON.parse(text);
|
|
34272
|
+
} catch {
|
|
34273
|
+
return { kind: "bad", state: "malformed" };
|
|
34274
|
+
}
|
|
34275
|
+
return fromValue(json2);
|
|
34276
|
+
}
|
|
34277
|
+
function samePin(a, b) {
|
|
34278
|
+
return a.ownerIdentityId === b.ownerIdentityId && a.keyAlgo === b.keyAlgo && a.publicKeyB64 === b.publicKeyB64;
|
|
34279
|
+
}
|
|
34280
|
+
function loadOwnerGrantPin(opts) {
|
|
34281
|
+
const fs = opts.fs ?? NODE_FS;
|
|
34282
|
+
const file2 = readPinFile(join16(dirname4(opts.credentialPath), PIN_FILE_NAME), fs);
|
|
34283
|
+
const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
|
|
34284
|
+
if (file2.kind === "bad") return { state: file2.state, pin: null };
|
|
34285
|
+
if (field.kind === "bad") return { state: field.state, pin: null };
|
|
34286
|
+
if (file2.kind === "absent" && field.kind === "absent") return { state: "no_pin", pin: null };
|
|
34287
|
+
if (file2.kind === "ok" && field.kind === "ok") {
|
|
34288
|
+
return samePin(file2.pin, field.pin) ? { state: "ok", pin: file2.pin } : { state: "pin_conflict", pin: null };
|
|
34289
|
+
}
|
|
34290
|
+
const one = file2.kind === "ok" ? file2.pin : field.pin;
|
|
34291
|
+
return { state: "ok", pin: one };
|
|
34292
|
+
}
|
|
34293
|
+
|
|
34294
|
+
// src/grants/manifest.ts
|
|
34295
|
+
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
34296
|
+
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
34297
|
+
owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
|
|
34298
|
+
team_room: Object.freeze(["web_search"])
|
|
34299
|
+
});
|
|
34300
|
+
function manifestSignedBytes(manifest) {
|
|
34301
|
+
const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
|
|
34302
|
+
const body = canonicalize(manifest);
|
|
34303
|
+
const out = new Uint8Array(domain.length + body.length);
|
|
34304
|
+
out.set(domain, 0);
|
|
34305
|
+
out.set(body, domain.length);
|
|
34306
|
+
return out;
|
|
34307
|
+
}
|
|
34308
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34309
|
+
function wellTyped(v) {
|
|
34310
|
+
if (typeof v === "string") return true;
|
|
34311
|
+
if (typeof v === "number") return Number.isSafeInteger(v) && v >= 0;
|
|
34312
|
+
if (Array.isArray(v)) return v.every(wellTyped);
|
|
34313
|
+
if (isPlainObject(v)) return Object.values(v).every(wellTyped);
|
|
34314
|
+
return false;
|
|
34315
|
+
}
|
|
34316
|
+
var WRAPPER_KEYS = ["algo", "manifest", "signature"];
|
|
34317
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
34318
|
+
var PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
|
|
34319
|
+
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII.test(v);
|
|
34320
|
+
var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
|
|
34321
|
+
var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
34322
|
+
function parseEndpoints(v) {
|
|
34323
|
+
if (v === void 0) return {};
|
|
34324
|
+
if (!isPlainObject(v)) return null;
|
|
34325
|
+
const out = {};
|
|
34326
|
+
for (const [name, raw] of Object.entries(v)) {
|
|
34327
|
+
if (!isPlainObject(raw)) return null;
|
|
34328
|
+
const e = { ...raw };
|
|
34329
|
+
if (e.headersFile === "") delete e.headersFile;
|
|
34330
|
+
if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
|
|
34331
|
+
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII.test(e.baseUrl)) return null;
|
|
34332
|
+
let url;
|
|
34333
|
+
try {
|
|
34334
|
+
url = new URL(e.baseUrl);
|
|
34335
|
+
} catch {
|
|
34336
|
+
return null;
|
|
34337
|
+
}
|
|
34338
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
34339
|
+
if (url.username !== "" || url.password !== "" || url.href !== e.baseUrl) return null;
|
|
34340
|
+
if ("headersFile" in e && !absolutePath(e.headersFile)) return null;
|
|
34341
|
+
out[name] = Object.freeze({ baseUrl: e.baseUrl, ...typeof e.headersFile === "string" ? { headersFile: e.headersFile } : {} });
|
|
34342
|
+
}
|
|
34343
|
+
return out;
|
|
34344
|
+
}
|
|
34345
|
+
function parseDatabases(v) {
|
|
34346
|
+
if (v === void 0) return {};
|
|
34347
|
+
if (!isPlainObject(v)) return null;
|
|
34348
|
+
const out = {};
|
|
34349
|
+
for (const [name, d] of Object.entries(v)) {
|
|
34350
|
+
if (!isPlainObject(d)) return null;
|
|
34351
|
+
const keys = Object.keys(d);
|
|
34352
|
+
if (keys.length !== 1 || keys[0] !== "sqlite" && keys[0] !== "postgresUrlFile") return null;
|
|
34353
|
+
const path = d[keys[0]];
|
|
34354
|
+
if (!absolutePath(path)) return null;
|
|
34355
|
+
out[name] = Object.freeze(keys[0] === "sqlite" ? { sqlite: path } : { postgresUrlFile: path });
|
|
34356
|
+
}
|
|
34357
|
+
return out;
|
|
34358
|
+
}
|
|
34359
|
+
function parseConfig(v) {
|
|
34360
|
+
if (!isPlainObject(v)) return null;
|
|
34361
|
+
if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
|
|
34362
|
+
if (typeof v.persona_sha256 !== "string" || !HEX64.test(v.persona_sha256)) return null;
|
|
34363
|
+
if (typeof v.legacy_assign_sha256 !== "string" || !HEX64.test(v.legacy_assign_sha256)) return null;
|
|
34364
|
+
if (!isStringArray(v.flags)) return null;
|
|
34365
|
+
if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
|
|
34366
|
+
if ("llm" in v) {
|
|
34367
|
+
const l = v.llm;
|
|
34368
|
+
if (!isPlainObject(l) || !exactKeys(l, ["provider", "model"]) || typeof l.provider !== "string" || !l.provider || typeof l.model !== "string" || !l.model) return null;
|
|
34369
|
+
}
|
|
34370
|
+
if ("search" in v) {
|
|
34371
|
+
const sr = v.search;
|
|
34372
|
+
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
|
|
34373
|
+
}
|
|
34374
|
+
const mem = v.memory;
|
|
34375
|
+
if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
|
|
34376
|
+
if (mem.mode !== "markdown" && mem.mode !== "semantic" && mem.mode !== "off") return null;
|
|
34377
|
+
if (mem.mode === "semantic" !== "recall_days" in mem) return null;
|
|
34378
|
+
if ("recall_days" in mem && mem.recall_days !== 30 && mem.recall_days !== 90 && mem.recall_days !== 365) return null;
|
|
34379
|
+
return Object.freeze({
|
|
34380
|
+
persona_sha256: v.persona_sha256,
|
|
34381
|
+
...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
|
|
34382
|
+
...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
|
|
34383
|
+
...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
|
|
34384
|
+
flags: Object.freeze([...v.flags]),
|
|
34385
|
+
memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
|
|
34386
|
+
legacy_assign_sha256: v.legacy_assign_sha256
|
|
34387
|
+
});
|
|
34388
|
+
}
|
|
34389
|
+
function verifyToolGrantManifest(raw, ctx) {
|
|
34390
|
+
if (!isPlainObject(raw)) return { state: "malformed" };
|
|
34391
|
+
const keys = Object.keys(raw).sort();
|
|
34392
|
+
if (keys.length !== WRAPPER_KEYS.length || keys.some((k, i) => k !== WRAPPER_KEYS[i])) return { state: "malformed" };
|
|
34393
|
+
if (typeof raw.algo !== "string") return { state: "malformed" };
|
|
34394
|
+
if (raw.algo !== "ed25519") return { state: "unsupported_algo" };
|
|
34395
|
+
const m = raw.manifest;
|
|
34396
|
+
if (!isPlainObject(m)) return { state: "malformed" };
|
|
34397
|
+
if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
|
|
34398
|
+
if (!wellTyped(m)) return { state: "malformed" };
|
|
34399
|
+
if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
|
|
34400
|
+
for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
|
|
34401
|
+
if (k in m && typeof m[k] !== "string") return { state: "malformed" };
|
|
34402
|
+
}
|
|
34403
|
+
if (!isPlainObject(m.grants)) return { state: "malformed" };
|
|
34404
|
+
for (const list of Object.values(m.grants)) {
|
|
34405
|
+
if (!Array.isArray(list) || !list.every((t) => typeof t === "string")) return { state: "malformed" };
|
|
34406
|
+
}
|
|
34407
|
+
const endpoints = parseEndpoints(m.endpoints);
|
|
34408
|
+
const databases = parseDatabases(m.databases);
|
|
34409
|
+
const config = parseConfig(m.config);
|
|
34410
|
+
if (endpoints === null || databases === null || config === null) return { state: "malformed" };
|
|
34411
|
+
const sig = canonicalBase64Bytes(raw.signature, 64);
|
|
34412
|
+
if (!sig) return { state: "malformed" };
|
|
34413
|
+
let good;
|
|
34414
|
+
try {
|
|
34415
|
+
good = ed255195.verify(sig, manifestSignedBytes(m), ctx.pin.publicKey, { zip215: false });
|
|
34416
|
+
} catch {
|
|
34417
|
+
return { state: "malformed" };
|
|
34418
|
+
}
|
|
34419
|
+
if (!good) return { state: "bad_signature" };
|
|
34420
|
+
if (m.agent_identity_id !== ctx.agentIdentityId) return { state: "wrong_agent" };
|
|
34421
|
+
if (m.owner_identity_id !== ctx.pin.ownerIdentityId) return { state: "wrong_owner" };
|
|
34422
|
+
const counter = m.counter;
|
|
34423
|
+
if (typeof counter !== "number" || !Number.isSafeInteger(counter) || counter < 1) return { state: "malformed" };
|
|
34424
|
+
if (ctx.storedCounter !== null) {
|
|
34425
|
+
if (counter === ctx.storedCounter) return { state: "unchanged" };
|
|
34426
|
+
if (counter < ctx.storedCounter) return { state: "rollback" };
|
|
34427
|
+
}
|
|
34428
|
+
let droppedUnknown = false;
|
|
34429
|
+
const grants = {};
|
|
34430
|
+
for (const [audience, list] of Object.entries(m.grants)) {
|
|
34431
|
+
if (audience !== "owner_dm" && audience !== "team_room") {
|
|
34432
|
+
droppedUnknown = true;
|
|
34433
|
+
continue;
|
|
34434
|
+
}
|
|
34435
|
+
const known = MANIFEST_KNOWN_TOOLS[audience];
|
|
34436
|
+
const kept = list.filter((t) => known.includes(t));
|
|
34437
|
+
if (kept.length !== list.length) droppedUnknown = true;
|
|
34438
|
+
grants[audience] = Object.freeze(kept);
|
|
34439
|
+
}
|
|
34440
|
+
const manifest = Object.freeze({
|
|
34441
|
+
type: "securecoms.tool-grants",
|
|
34442
|
+
v: 1,
|
|
34443
|
+
agent_identity_id: m.agent_identity_id,
|
|
34444
|
+
owner_identity_id: m.owner_identity_id,
|
|
34445
|
+
...typeof m.owner_dm_channel_id === "string" ? { owner_dm_channel_id: m.owner_dm_channel_id } : {},
|
|
34446
|
+
...typeof m.owner_dm_relay_identity_id === "string" ? { owner_dm_relay_identity_id: m.owner_dm_relay_identity_id } : {},
|
|
34447
|
+
counter,
|
|
34448
|
+
grants: Object.freeze(grants),
|
|
34449
|
+
endpoints: Object.freeze(endpoints),
|
|
34450
|
+
databases: Object.freeze(databases),
|
|
34451
|
+
config
|
|
34452
|
+
});
|
|
34453
|
+
return { state: "ok", manifest, droppedUnknown, wrapper: raw };
|
|
34454
|
+
}
|
|
34455
|
+
|
|
34456
|
+
// src/grants/persona-render.ts
|
|
34457
|
+
var PERSONA_MAX_BYTES = 8192;
|
|
34458
|
+
var TONE_FIELD_ORDER = ["humor", "length", "warmth", "formality"];
|
|
34459
|
+
var TONE_WORDS = Object.freeze({
|
|
34460
|
+
warmth: { reserved: "Be plain and businesslike.", plain: "Be plain and businesslike.", warm: "Be warm.", friendly: "Be friendly." },
|
|
34461
|
+
length: { brief: "Keep answers short.", thorough: "Give the full picture when it matters." },
|
|
34462
|
+
formality: { casual: "Write casually.", formal: "Write formally." },
|
|
34463
|
+
humor: { none: "Do not use humor.", light: "Use light humor when it fits.", playful: "Be playful." }
|
|
34464
|
+
});
|
|
34465
|
+
var HANDOFF_TEXT = Object.freeze({
|
|
34466
|
+
money: "money is moving",
|
|
34467
|
+
legal: "someone wants a legal commitment",
|
|
34468
|
+
upset: "the person is upset"
|
|
34469
|
+
});
|
|
34470
|
+
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.";
|
|
34471
|
+
var PY_SPACE = /* @__PURE__ */ new Set([
|
|
34472
|
+
9,
|
|
34473
|
+
10,
|
|
34474
|
+
11,
|
|
34475
|
+
12,
|
|
34476
|
+
13,
|
|
34477
|
+
28,
|
|
34478
|
+
29,
|
|
34479
|
+
30,
|
|
34480
|
+
31,
|
|
34481
|
+
32,
|
|
34482
|
+
133,
|
|
34483
|
+
160,
|
|
34484
|
+
5760,
|
|
34485
|
+
8192,
|
|
34486
|
+
8193,
|
|
34487
|
+
8194,
|
|
34488
|
+
8195,
|
|
34489
|
+
8196,
|
|
34490
|
+
8197,
|
|
34491
|
+
8198,
|
|
34492
|
+
8199,
|
|
34493
|
+
8200,
|
|
34494
|
+
8201,
|
|
34495
|
+
8202,
|
|
34496
|
+
8232,
|
|
34497
|
+
8233,
|
|
34498
|
+
8239,
|
|
34499
|
+
8287,
|
|
34500
|
+
12288
|
|
34501
|
+
]);
|
|
34502
|
+
function pyStrip(s) {
|
|
34503
|
+
let a = 0;
|
|
34504
|
+
let b = s.length;
|
|
34505
|
+
while (a < b && PY_SPACE.has(s.charCodeAt(a))) a++;
|
|
34506
|
+
while (b > a && PY_SPACE.has(s.charCodeAt(b - 1))) b--;
|
|
34507
|
+
return s.slice(a, b);
|
|
34508
|
+
}
|
|
34509
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34510
|
+
var optString = (v) => v === void 0 || v === null || typeof v === "string";
|
|
34511
|
+
var optStrings = (v) => v === void 0 || v === null || Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
34512
|
+
function lines(items) {
|
|
34513
|
+
return (items ?? []).map(pyStrip).filter((x) => x !== "").map((x) => `- ${x}`).join("\n");
|
|
34514
|
+
}
|
|
34515
|
+
function renderPersonaFiles(persona) {
|
|
34516
|
+
if (!isObj(persona)) return { ok: false, reason: "shape" };
|
|
34517
|
+
const p = persona;
|
|
34518
|
+
for (const k of ["name", "business", "role", "manner"]) if (!optString(p[k])) return { ok: false, reason: "shape" };
|
|
34519
|
+
for (const k of ["can", "cannot", "handoff"]) if (!optStrings(p[k])) return { ok: false, reason: "shape" };
|
|
34520
|
+
const toneRaw = p.tone;
|
|
34521
|
+
if (!(toneRaw === void 0 || toneRaw === null || isObj(toneRaw) && Object.values(toneRaw).every((v) => typeof v === "string"))) {
|
|
34522
|
+
return { ok: false, reason: "shape" };
|
|
34523
|
+
}
|
|
34524
|
+
const name = pyStrip(p.name || "");
|
|
34525
|
+
const business = pyStrip(p.business || "");
|
|
34526
|
+
const role = pyStrip(p.role || "");
|
|
34527
|
+
const identity = [];
|
|
34528
|
+
if (name && business) identity.push(`You are ${name}, an agent for ${business}.`);
|
|
34529
|
+
else if (name) identity.push(`You are ${name}.`);
|
|
34530
|
+
if (role) identity.push(role);
|
|
34531
|
+
const identityMd = identity.join("\n\n");
|
|
34532
|
+
const tone = toneRaw ?? {};
|
|
34533
|
+
const soul = [];
|
|
34534
|
+
for (const k of TONE_FIELD_ORDER) {
|
|
34535
|
+
if (!Object.prototype.hasOwnProperty.call(tone, k)) continue;
|
|
34536
|
+
const words = TONE_WORDS[k];
|
|
34537
|
+
const v = tone[k];
|
|
34538
|
+
if (Object.prototype.hasOwnProperty.call(words, v)) soul.push(words[v]);
|
|
34539
|
+
}
|
|
34540
|
+
const manner = pyStrip(p.manner || "");
|
|
34541
|
+
let soulMd = soul.join(" ");
|
|
34542
|
+
if (manner) soulMd = pyStrip(`${soulMd}
|
|
34543
|
+
|
|
34544
|
+
${manner}`);
|
|
34545
|
+
const agents = [];
|
|
34546
|
+
const can = lines(p.can);
|
|
34547
|
+
if (can) agents.push(`What you can do:
|
|
34548
|
+
${can}`);
|
|
34549
|
+
const cannot = lines(p.cannot);
|
|
34550
|
+
if (cannot) agents.push(`What you cannot do:
|
|
34551
|
+
${cannot}`);
|
|
34552
|
+
const handoff = (p.handoff ?? []).filter((h) => pyStrip(h) !== "").map((h) => Object.prototype.hasOwnProperty.call(HANDOFF_TEXT, h) ? HANDOFF_TEXT[h] : pyStrip(h));
|
|
34553
|
+
if (handoff.length > 0) agents.push("Hand off to a person when " + handoff.join("; when ") + ". Say plainly that you are doing so.");
|
|
34554
|
+
agents.push(TOOL_PARAGRAPH);
|
|
34555
|
+
const agentsMd = agents.join("\n\n");
|
|
34556
|
+
const files = { "IDENTITY.md": identityMd, "SOUL.md": soulMd, "AGENTS.md": agentsMd };
|
|
34557
|
+
for (const body of Object.values(files)) {
|
|
34558
|
+
if (Buffer.byteLength(body, "utf8") > PERSONA_MAX_BYTES) return { ok: false, reason: "over_cap" };
|
|
34559
|
+
}
|
|
34560
|
+
return { ok: true, files };
|
|
34561
|
+
}
|
|
34562
|
+
|
|
34563
|
+
// src/grants/binding.ts
|
|
34564
|
+
var CONFIG_BINDING_FIELDS = ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"];
|
|
34565
|
+
var ASSIGN_PREFIX = "legacy-fact-assign:";
|
|
34566
|
+
function legacyAssignSha256(prunes) {
|
|
34567
|
+
const set = [...new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)))].sort();
|
|
34568
|
+
return sha256Hex(canonicalize(set));
|
|
34569
|
+
}
|
|
34570
|
+
function legacyAssignToken(d) {
|
|
34571
|
+
return `${ASSIGN_PREFIX}${d.source_id}:${d.fact_hash}:${d.channel_id}`;
|
|
34572
|
+
}
|
|
34573
|
+
var PERSONA_FILES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
|
|
34574
|
+
var LLM_SOURCE = "llm-source.json";
|
|
34575
|
+
var FAIL = Symbol("bind_fail");
|
|
34576
|
+
function guarded(f) {
|
|
34577
|
+
try {
|
|
34578
|
+
return f();
|
|
34579
|
+
} catch {
|
|
34580
|
+
return FAIL;
|
|
34581
|
+
}
|
|
34582
|
+
}
|
|
34583
|
+
var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34584
|
+
var strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? [...v] : void 0;
|
|
34585
|
+
var sameStrings = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((x, i) => x === b[i]);
|
|
34586
|
+
var sha256Hex = (bytes) => Buffer.from(sha25611(bytes)).toString("hex");
|
|
34587
|
+
function endpointEquals(raw, signed) {
|
|
34588
|
+
if (!isObj2(raw)) return false;
|
|
34589
|
+
const e = { ...raw };
|
|
34590
|
+
if (e.headersFile === "") delete e.headersFile;
|
|
34591
|
+
const keys = Object.keys(e).sort().join(",");
|
|
34592
|
+
const want = "headersFile" in signed ? "baseUrl,headersFile" : "baseUrl";
|
|
34593
|
+
return keys === want && e.baseUrl === signed.baseUrl && e.headersFile === signed.headersFile;
|
|
34594
|
+
}
|
|
34595
|
+
function databaseEquals(raw, signed) {
|
|
34596
|
+
if (!isObj2(raw)) return false;
|
|
34597
|
+
const keys = Object.keys(raw);
|
|
34598
|
+
if (keys.length !== 1) return false;
|
|
34599
|
+
return "sqlite" in signed ? raw.sqlite === signed.sqlite : raw.postgresUrlFile === signed.postgresUrlFile;
|
|
34600
|
+
}
|
|
34601
|
+
function bindTargets(raw, signed, equals) {
|
|
34602
|
+
if (raw === void 0) return { written: {}, mismatches: 0 };
|
|
34603
|
+
if (!isObj2(raw)) return { written: {}, mismatches: 1 };
|
|
34604
|
+
const written = {};
|
|
34605
|
+
let mismatches = 0;
|
|
34606
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
34607
|
+
const s = Object.prototype.hasOwnProperty.call(signed, name) ? signed[name] : void 0;
|
|
34608
|
+
if (s !== void 0 && equals(entry, s)) written[name] = s;
|
|
34609
|
+
else mismatches++;
|
|
34610
|
+
}
|
|
34611
|
+
return { written, mismatches };
|
|
34612
|
+
}
|
|
34613
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
|
|
34614
|
+
const mismatches = /* @__PURE__ */ new Set();
|
|
34615
|
+
const cfg = manifest.config;
|
|
34616
|
+
const out = {};
|
|
34617
|
+
const files = isObj2(payload.files) ? payload.files : {};
|
|
34618
|
+
const personaOk = guarded(() => sha256Hex(canonicalize(payload.persona)) === cfg.persona_sha256);
|
|
34619
|
+
const render = guarded(() => renderPersonaFiles(payload.persona));
|
|
34620
|
+
if (personaOk === true && render !== FAIL && render.ok) {
|
|
34621
|
+
for (const f of PERSONA_FILES) out[f] = render.files[f];
|
|
34622
|
+
} else {
|
|
34623
|
+
mismatches.add("persona");
|
|
34624
|
+
}
|
|
34625
|
+
const delivered = files[LLM_SOURCE];
|
|
34626
|
+
const llmSame = guarded(() => {
|
|
34627
|
+
if (delivered === void 0) return cfg.llm === void 0;
|
|
34628
|
+
if (typeof delivered !== "string") return false;
|
|
34629
|
+
const parsed = JSON.parse(delivered);
|
|
34630
|
+
if (!isObj2(parsed)) return false;
|
|
34631
|
+
if (cfg.llm === void 0) return parsed.provider === void 0 && parsed.model === void 0;
|
|
34632
|
+
return parsed.provider === cfg.llm.provider && parsed.model === cfg.llm.model;
|
|
34633
|
+
});
|
|
34634
|
+
if (llmSame !== true) mismatches.add("llm");
|
|
34635
|
+
if (cfg.llm !== void 0) {
|
|
34636
|
+
out[LLM_SOURCE] = JSON.stringify({ provider: cfg.llm.provider, model: cfg.llm.model, version: payload.version }) + "\n";
|
|
34637
|
+
}
|
|
34638
|
+
if (!isObj2(payload.files) && payload.files !== void 0) mismatches.add("files");
|
|
34639
|
+
for (const name of Object.keys(files)) {
|
|
34640
|
+
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
34641
|
+
}
|
|
34642
|
+
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
34643
|
+
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
34644
|
+
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
34645
|
+
const payloadFlags = strings(sj?.flags);
|
|
34646
|
+
if (!sameStrings(payloadFlags, cfg.flags)) mismatches.add("flags");
|
|
34647
|
+
const tools = isObj2(sj?.tools) ? sj.tools : void 0;
|
|
34648
|
+
if (!sameStrings(strings(tools?.allow), cfg.tools_allow)) mismatches.add("tools_allow");
|
|
34649
|
+
const deny = strings(tools?.deny);
|
|
34650
|
+
const ps = isObj2(sj?.search) ? sj.search : void 0;
|
|
34651
|
+
const payloadProvider = ps?.provider;
|
|
34652
|
+
let searchBlock;
|
|
34653
|
+
let appliedSearch;
|
|
34654
|
+
if (payloadProvider === cfg.search?.provider && ps === void 0 === (cfg.search === void 0)) {
|
|
34655
|
+
searchBlock = ps === void 0 ? null : Object.freeze({ provider: ps.provider, ...typeof ps.keyFile === "string" ? { keyFile: ps.keyFile } : {} });
|
|
34656
|
+
appliedSearch = searchBlock;
|
|
34657
|
+
} else {
|
|
34658
|
+
mismatches.add("search");
|
|
34659
|
+
searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
|
|
34660
|
+
}
|
|
34661
|
+
const skillsDoc = {
|
|
34662
|
+
endpoints: eps.written,
|
|
34663
|
+
databases: dbs.written,
|
|
34664
|
+
flags: [...cfg.flags],
|
|
34665
|
+
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
34666
|
+
...searchBlock ? { search: searchBlock } : {}
|
|
34667
|
+
};
|
|
34668
|
+
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
34669
|
+
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
34670
|
+
let memory;
|
|
34671
|
+
if (isObj2(payload.memory)) {
|
|
34672
|
+
const pm = payload.memory;
|
|
34673
|
+
const sameMode = pm.mode === cfg.memory.mode;
|
|
34674
|
+
const sameDays = cfg.memory.mode === "semantic" ? pm.recall_days === cfg.memory.recall_days : !("recall_days" in pm);
|
|
34675
|
+
if (!sameMode || !sameDays) mismatches.add("memory");
|
|
34676
|
+
const { recall_days: _r, mode: _m, ...rest } = pm;
|
|
34677
|
+
memory = { ...signedMemory, ...rest };
|
|
34678
|
+
} else {
|
|
34679
|
+
mismatches.add("memory");
|
|
34680
|
+
memory = { ...signedMemory };
|
|
34681
|
+
}
|
|
34682
|
+
const prunes = payload.memory_prunes === void 0 ? [] : payload.memory_prunes;
|
|
34683
|
+
const decisions = Array.isArray(payload.legacy_memory_fact_decisions) ? payload.legacy_memory_fact_decisions : [];
|
|
34684
|
+
const assignMatch = guarded(() => Array.isArray(prunes) && legacyAssignSha256(prunes) === cfg.legacy_assign_sha256);
|
|
34685
|
+
let legacyDecisions;
|
|
34686
|
+
if (assignMatch === true) {
|
|
34687
|
+
const verified = new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)));
|
|
34688
|
+
legacyDecisions = decisions.filter((d) => {
|
|
34689
|
+
if (!isObj2(d) || d.action !== "assign") return true;
|
|
34690
|
+
if (typeof d.source_id !== "string" || typeof d.fact_hash !== "string" || typeof d.channel_id !== "string") return false;
|
|
34691
|
+
return verified.has(legacyAssignToken({ source_id: d.source_id, fact_hash: d.fact_hash, channel_id: d.channel_id }));
|
|
34692
|
+
});
|
|
34693
|
+
} else {
|
|
34694
|
+
mismatches.add("legacy_assign");
|
|
34695
|
+
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
34696
|
+
}
|
|
34697
|
+
const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
|
|
34698
|
+
const policy = {
|
|
34699
|
+
v: 1,
|
|
34700
|
+
manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
|
|
34701
|
+
secret_path_mode: "enforce"
|
|
34702
|
+
};
|
|
34703
|
+
return {
|
|
34704
|
+
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
34705
|
+
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
34706
|
+
files: out,
|
|
34707
|
+
memory,
|
|
34708
|
+
signedTargets: { endpoints: Object.keys(eps.written).length, databases: Object.keys(dbs.written).length },
|
|
34709
|
+
policy,
|
|
34710
|
+
appliedSearch,
|
|
34711
|
+
legacyDecisions
|
|
34712
|
+
};
|
|
34713
|
+
}
|
|
34714
|
+
|
|
34715
|
+
// src/grants/room-search-cap.ts
|
|
34716
|
+
var ROOM_SEARCH_DAILY_CAP = 50;
|
|
34717
|
+
var ROOM_SEARCH_CAP_ENV = "SECURECOMS_ROOM_SEARCH_DAILY_CAP";
|
|
34718
|
+
function parseRoomSearchCap(v, warn) {
|
|
34719
|
+
if (v === void 0) return ROOM_SEARCH_DAILY_CAP;
|
|
34720
|
+
if (/^\d+$/.test(v)) {
|
|
34721
|
+
const n = Number(v);
|
|
34722
|
+
if (n <= ROOM_SEARCH_DAILY_CAP) return n;
|
|
34723
|
+
}
|
|
34724
|
+
warn?.();
|
|
34725
|
+
return ROOM_SEARCH_DAILY_CAP;
|
|
34726
|
+
}
|
|
34727
|
+
var AGENT_ROOM_SEARCH_DAILY_CAP = 200;
|
|
34728
|
+
var AGENT_ROOM_SEARCH_CAP_ENV = "SECURECOMS_AGENT_ROOM_SEARCH_DAILY_CAP";
|
|
34729
|
+
function parseAgentRoomSearchCap(v, warn) {
|
|
34730
|
+
if (v === void 0) return AGENT_ROOM_SEARCH_DAILY_CAP;
|
|
34731
|
+
if (/^\d+$/.test(v)) {
|
|
34732
|
+
const n = Number(v);
|
|
34733
|
+
if (n <= AGENT_ROOM_SEARCH_DAILY_CAP) return n;
|
|
34734
|
+
}
|
|
34735
|
+
warn?.();
|
|
34736
|
+
return AGENT_ROOM_SEARCH_DAILY_CAP;
|
|
34737
|
+
}
|
|
34738
|
+
var AGENT_TOTAL_KEY = "agent-total";
|
|
34739
|
+
|
|
34740
|
+
// src/grants/effective.ts
|
|
34741
|
+
var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
|
|
34742
|
+
var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
|
|
34743
|
+
function effectiveToolGrants(i) {
|
|
34744
|
+
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
|
|
34745
|
+
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
|
|
34746
|
+
const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
|
|
34747
|
+
const listed = new Set(i.manifest.grants[i.audience] ?? []);
|
|
34748
|
+
const signed = ceiling.filter((t) => listed.has(t));
|
|
34749
|
+
if (!i.eligible || !i.manifestSwitchOn) {
|
|
34750
|
+
const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
|
|
34751
|
+
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
|
|
34752
|
+
}
|
|
34753
|
+
let withheld = false;
|
|
34754
|
+
const tools = signed.filter((t) => {
|
|
34755
|
+
if (i.disabled.has(t)) return false;
|
|
34756
|
+
if (t === "http_request" && i.signedTargets.endpoints === 0) return false;
|
|
34757
|
+
if (t === "sql_query" && i.signedTargets.databases === 0) return false;
|
|
34758
|
+
if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
|
|
34759
|
+
withheld = true;
|
|
34760
|
+
return false;
|
|
34761
|
+
}
|
|
34762
|
+
return true;
|
|
34763
|
+
});
|
|
34764
|
+
return {
|
|
34765
|
+
grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
|
|
34766
|
+
source: "manifest",
|
|
34767
|
+
withheldBySearchCap: withheld
|
|
34768
|
+
};
|
|
34769
|
+
}
|
|
34770
|
+
|
|
33822
34771
|
// src/bridge-agent.ts
|
|
33823
34772
|
import { homedir as homedir4 } from "node:os";
|
|
33824
34773
|
|
|
@@ -33907,6 +34856,50 @@ function roomIsCrossOrg(rows) {
|
|
|
33907
34856
|
return unknown ? null : false;
|
|
33908
34857
|
}
|
|
33909
34858
|
|
|
34859
|
+
// src/grants/owner-turn.ts
|
|
34860
|
+
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
34861
|
+
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
34862
|
+
function verifyOwnerSignature(entry, pin) {
|
|
34863
|
+
if (!entry.identitySignature || !entry.payloadHex) return { state: "no_signature" };
|
|
34864
|
+
if (!HEX.test(entry.payloadHex)) return { state: "malformed" };
|
|
34865
|
+
const sig = canonicalBase64Bytes(entry.identitySignature, 64);
|
|
34866
|
+
if (!sig) return { state: "malformed" };
|
|
34867
|
+
const bytes = new Uint8Array(Buffer.from(entry.payloadHex, "hex"));
|
|
34868
|
+
let envelope;
|
|
34869
|
+
try {
|
|
34870
|
+
envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
34871
|
+
} catch {
|
|
34872
|
+
return { state: "malformed" };
|
|
34873
|
+
}
|
|
34874
|
+
const algo = envelope?.identity_proof?.algo;
|
|
34875
|
+
if (typeof envelope !== "object" || envelope === null || algo === void 0 || algo === null) return { state: "malformed" };
|
|
34876
|
+
if (algo !== "ed25519") return { state: "unknown_algo" };
|
|
34877
|
+
let good;
|
|
34878
|
+
try {
|
|
34879
|
+
good = ed255196.verify(sig, bytes, pin.publicKey, { zip215: false });
|
|
34880
|
+
} catch {
|
|
34881
|
+
return { state: "malformed" };
|
|
34882
|
+
}
|
|
34883
|
+
return good ? { state: "ok", envelope } : { state: "bad_signature" };
|
|
34884
|
+
}
|
|
34885
|
+
function classifyOwnerDmLeaves(leaves, allowed) {
|
|
34886
|
+
if (leaves.some((l) => !l.basic || l.identity === null)) return "foreign";
|
|
34887
|
+
if (!leaves.some((l) => l.identity === allowed.ownerIdentityId)) return "foreign";
|
|
34888
|
+
const unexpected = /* @__PURE__ */ new Set();
|
|
34889
|
+
for (const l of leaves) {
|
|
34890
|
+
const id = l.identity;
|
|
34891
|
+
if (id === allowed.ownerIdentityId || id === allowed.agentIdentityId) continue;
|
|
34892
|
+
if (allowed.relayIdentityId !== void 0 && id === allowed.relayIdentityId) continue;
|
|
34893
|
+
unexpected.add(id);
|
|
34894
|
+
}
|
|
34895
|
+
if (unexpected.size === 0) return "ok";
|
|
34896
|
+
if (unexpected.size === 1) {
|
|
34897
|
+
const [only] = unexpected;
|
|
34898
|
+
if (allowed.isServerRelay(only)) return "unsigned_relay";
|
|
34899
|
+
}
|
|
34900
|
+
return "foreign";
|
|
34901
|
+
}
|
|
34902
|
+
|
|
33910
34903
|
// src/bridge-agent.ts
|
|
33911
34904
|
function logSendSupersededAdoption(kind2) {
|
|
33912
34905
|
switch (kind2) {
|
|
@@ -33959,6 +34952,8 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
33959
34952
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
33960
34953
|
]);
|
|
33961
34954
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
34955
|
+
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
34956
|
+
var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
|
|
33962
34957
|
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
33963
34958
|
const v = env.SECURECOMS_OWNER_DM_DEFAULT_GRANTS;
|
|
33964
34959
|
return v === void 0 || v === "on";
|
|
@@ -33967,6 +34962,14 @@ function roomDefaultGrantsEnabled(env = process.env) {
|
|
|
33967
34962
|
const v = env.SECURECOMS_ROOM_DEFAULT_GRANTS;
|
|
33968
34963
|
return v === void 0 || v === "on";
|
|
33969
34964
|
}
|
|
34965
|
+
function ownerDmManifestGrantsEnabled(env = process.env) {
|
|
34966
|
+
const v = env.SECURECOMS_OWNER_DM_MANIFEST_GRANTS;
|
|
34967
|
+
return v === void 0 || v === "on";
|
|
34968
|
+
}
|
|
34969
|
+
function roomManifestGrantsEnabled(env = process.env) {
|
|
34970
|
+
const v = env.SECURECOMS_ROOM_MANIFEST_GRANTS;
|
|
34971
|
+
return v === void 0 || v === "on";
|
|
34972
|
+
}
|
|
33970
34973
|
var MAX_PENDING_NOTE_REQUESTS = 8;
|
|
33971
34974
|
var INGEST_STAMP_KEY = "_sc_notes_ingest_generation";
|
|
33972
34975
|
function validIngestStamp(v) {
|
|
@@ -34053,6 +35056,17 @@ function redeliveryDelayMs(attempts) {
|
|
|
34053
35056
|
return 2 ** Math.min(Math.max(attempts, 0), 16) * REDELIVERY_BACKOFF_BASE_MS;
|
|
34054
35057
|
}
|
|
34055
35058
|
var DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
|
|
35059
|
+
function holdsManifestGrant(a) {
|
|
35060
|
+
return a?.tool_grants?.some((g) => g.mode === "read" && g.source === "manifest") ?? false;
|
|
35061
|
+
}
|
|
35062
|
+
function greetingAuthorization(a) {
|
|
35063
|
+
const { memory_write: _w, ...rest } = a;
|
|
35064
|
+
return Object.freeze({ ...rest, tool_grants: NO_TOOL_GRANTS });
|
|
35065
|
+
}
|
|
35066
|
+
function utcDayOf(ms) {
|
|
35067
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
35068
|
+
}
|
|
35069
|
+
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
35070
|
var DEAD_LETTER_NOTICE_INTERVAL_MS = 60 * 6e4;
|
|
34057
35071
|
var PAUSE_RECHECK_MS = 3e4;
|
|
34058
35072
|
var SUPERSEDE_RECHECK_MS = 3e4;
|
|
@@ -34181,7 +35195,7 @@ var FileOwnerAuthorityStore = class {
|
|
|
34181
35195
|
async save(enclaveId, pin) {
|
|
34182
35196
|
const all = await this.readAll();
|
|
34183
35197
|
all[enclaveId] = pin;
|
|
34184
|
-
await fsp.mkdir(
|
|
35198
|
+
await fsp.mkdir(dirname5(this.path), { recursive: true });
|
|
34185
35199
|
await fsp.writeFile(this.path, JSON.stringify(all), "utf8");
|
|
34186
35200
|
}
|
|
34187
35201
|
};
|
|
@@ -34340,6 +35354,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34340
35354
|
});
|
|
34341
35355
|
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
34342
35356
|
this.home = deps.home ?? homedir4();
|
|
35357
|
+
this.grantWriters = {
|
|
35358
|
+
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
35359
|
+
writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
|
|
35360
|
+
bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
|
|
35361
|
+
removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
|
|
35362
|
+
};
|
|
34343
35363
|
this.platform = deps.platform ?? process.platform;
|
|
34344
35364
|
this.machinePaths = deps.hostMachine === true || deps.home !== void 0;
|
|
34345
35365
|
this.unifiedOn = unifiedPart1On(deps.unifiedMemoryPart1);
|
|
@@ -34354,7 +35374,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34354
35374
|
threshold: EMBEDDING_MANIFEST.similarityThreshold,
|
|
34355
35375
|
warm: windowsSemanticOn(this.platform)
|
|
34356
35376
|
};
|
|
34357
|
-
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init,
|
|
35377
|
+
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname5(fileURLToPath2(import.meta.url)));
|
|
34358
35378
|
}
|
|
34359
35379
|
});
|
|
34360
35380
|
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 +35385,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34365
35385
|
modelId: EMBEDDING_MANIFEST.modelId,
|
|
34366
35386
|
threshold: EMBEDDING_MANIFEST.similarityThreshold
|
|
34367
35387
|
});
|
|
34368
|
-
this.heartbeat = new CallHeartbeat(this.machinePaths ?
|
|
35388
|
+
this.heartbeat = new CallHeartbeat(this.machinePaths ? join17(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
|
|
34369
35389
|
this.reembed = new ReembedQueue({
|
|
34370
35390
|
store: this.store,
|
|
34371
35391
|
embedder: this.embedder,
|
|
@@ -34447,8 +35467,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34447
35467
|
const j = await res.json();
|
|
34448
35468
|
return String(j.attachment_id);
|
|
34449
35469
|
};
|
|
34450
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
34451
|
-
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(
|
|
35470
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join17(dirname5(cfg.storePath), "attachments");
|
|
35471
|
+
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join17(dirname5(cfg.storePath), "vault-owner-pins.json"));
|
|
34452
35472
|
this.action = {
|
|
34453
35473
|
begin: (channelId) => this.actionBegin(channelId),
|
|
34454
35474
|
start: (opts) => this.actionStart(opts),
|
|
@@ -34466,6 +35486,30 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34466
35486
|
fetchImpl;
|
|
34467
35487
|
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
34468
35488
|
home;
|
|
35489
|
+
grantWriters;
|
|
35490
|
+
/** Owner grants (spec §7.5, OQ-2): the operator cap, read once; the same variable the runtime reads. 0 = no room search. */
|
|
35491
|
+
roomSearchDailyCap = parseRoomSearchCap(process.env[ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("cap_invalid", ["cap_invalid"]) }));
|
|
35492
|
+
/** Review M-3 (ERIC APPROVALS 3): the per-agent total across rooms, read once; the same variable the runtime reads. */
|
|
35493
|
+
agentRoomSearchDailyCap = parseAgentRoomSearchCap(process.env[AGENT_ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("agent_cap_invalid", ["agent_cap_invalid"]) }));
|
|
35494
|
+
/** `${day}\n${channel}` keys already logged reached today (in memory; the store holds the durable flag). */
|
|
35495
|
+
roomCapLogged = /* @__PURE__ */ new Set();
|
|
35496
|
+
/** Rooms whose K-14 send is in flight: a second frame posts nothing more. */
|
|
35497
|
+
roomCapNoticePending = /* @__PURE__ */ new Set();
|
|
35498
|
+
/** Held entries whose staged copy lost web_search to the backstop; applied again at release. */
|
|
35499
|
+
backstopStripped = /* @__PURE__ */ new Set();
|
|
35500
|
+
/**
|
|
35501
|
+
* Owner grants plan 4 Task 11 (P-4, P-6): what the managed directory holds under the current manifest.
|
|
35502
|
+
* Manifest grants require state "bound" at the applied counter (Task 13). null at startup, while a
|
|
35503
|
+
* refetch narrows it, and after any throw (the fallback).
|
|
35504
|
+
*/
|
|
35505
|
+
manifestBinding = null;
|
|
35506
|
+
/** skills.json tools.deny from the latest fetch, manifest or not (spec §5.2 `disabled`). */
|
|
35507
|
+
payloadToolDeny = /* @__PURE__ */ new Set();
|
|
35508
|
+
/** Spec §7.7 (OQ-5): the latest fetch's binding outcome, reported in tool_grant_status (Task 15). */
|
|
35509
|
+
configBindingMismatches = [];
|
|
35510
|
+
targetMismatches = { endpoint: 0, database: 0 };
|
|
35511
|
+
/** P-5 (Task 14): the legacy fact decisions the latest bound fetch may apply (assigns verified against the signed set). */
|
|
35512
|
+
boundLegacyDecisions = [];
|
|
34469
35513
|
joinState = /* @__PURE__ */ new Map();
|
|
34470
35514
|
/**
|
|
34471
35515
|
* Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
|
|
@@ -34769,11 +35813,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
34769
35813
|
ownerDmDefaultGrants = ownerDmDefaultGrantsEnabled();
|
|
34770
35814
|
/** Read once at startup; see roomDefaultGrantsEnabled. */
|
|
34771
35815
|
roomDefaultGrants = roomDefaultGrantsEnabled();
|
|
35816
|
+
/** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
|
|
35817
|
+
ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
|
|
35818
|
+
roomManifestGrants = roomManifestGrantsEnabled();
|
|
34772
35819
|
/**
|
|
34773
35820
|
* Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
|
|
34774
35821
|
* cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
|
|
34775
35822
|
*/
|
|
34776
35823
|
ownerDmCycleFresh = /* @__PURE__ */ new Set();
|
|
35824
|
+
// Owner-signed tool grants, step 1 (plan 1 task 7). The verified manifest is
|
|
35825
|
+
// RECORDED here and in the sealed store; no grant code reads it in step 1.
|
|
35826
|
+
toolGrantManifest = null;
|
|
35827
|
+
toolGrantCounter = null;
|
|
35828
|
+
manifestState = "no_manifest";
|
|
35829
|
+
/** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
|
|
35830
|
+
ownerVerifyFailures = 0;
|
|
35831
|
+
/** Verified owner turns of this ingest cycle, recorded in owner_replay when staged (P-3). */
|
|
35832
|
+
ownerReplayPending = /* @__PURE__ */ new Map();
|
|
35833
|
+
/** D-9: ids of owner turns that failed verification (never `replay`, audit E-3), id -> channel. Persisted too. */
|
|
35834
|
+
ownerVerifyFailedIds = /* @__PURE__ */ new Map();
|
|
35835
|
+
serverOwnerMismatchLogged = false;
|
|
35836
|
+
/** Audit L-2: the replay prune runs at most once per interval, on a monotonic clock. */
|
|
35837
|
+
monoNow = () => performance.now();
|
|
35838
|
+
lastOwnerReplayPruneAt = -Infinity;
|
|
34777
35839
|
/**
|
|
34778
35840
|
* Audit M-B1: room-granted turns, keyed by the inbound message id, with the
|
|
34779
35841
|
* channel, MLS group and epoch the room was classified at (review I-1: an
|
|
@@ -35216,14 +36278,38 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35216
36278
|
return;
|
|
35217
36279
|
}
|
|
35218
36280
|
const version = payload.version;
|
|
35219
|
-
|
|
36281
|
+
const skillsForDeny = payload.skills_json;
|
|
36282
|
+
const denyRaw = skillsForDeny?.tools?.deny;
|
|
36283
|
+
this.payloadToolDeny = new Set(Array.isArray(denyRaw) ? denyRaw.filter((t) => typeof t === "string") : []);
|
|
36284
|
+
let manifestStepThrew = false;
|
|
36285
|
+
try {
|
|
36286
|
+
await this.applyToolGrantManifest(payload.tool_grant_manifest);
|
|
36287
|
+
} catch (e) {
|
|
36288
|
+
manifestStepThrew = true;
|
|
36289
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
36290
|
+
}
|
|
36291
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
36292
|
+
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
36293
|
+
if (inForce !== null && pin !== null) {
|
|
36294
|
+
const applied = manifestStepThrew ? await this.bindFailed(inForce, payload.memory, void 0) : await this.applyUnderManifest(inForce, pin.publicKeyB64, payload, version);
|
|
36295
|
+
if (!applied) {
|
|
36296
|
+
await this.postRuntimeConfigReport(base, version, payload.skills_json ?? {});
|
|
36297
|
+
return;
|
|
36298
|
+
}
|
|
36299
|
+
} else {
|
|
36300
|
+
this.manifestBinding = null;
|
|
36301
|
+
this.configBindingMismatches = [];
|
|
36302
|
+
this.targetMismatches = { endpoint: 0, database: 0 };
|
|
36303
|
+
await this.applyPayloadMemory(payload.memory);
|
|
36304
|
+
this.overwriteStalePolicy();
|
|
36305
|
+
}
|
|
35220
36306
|
if (!version) {
|
|
35221
36307
|
writeManagedFiles(this.home, this.cfg.identityId, {});
|
|
35222
36308
|
writeServiceLog2("info", "runtime_config.not_saved");
|
|
35223
36309
|
}
|
|
35224
36310
|
const skillsJson = payload.skills_json ?? {};
|
|
35225
|
-
if (version) {
|
|
35226
|
-
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
36311
|
+
if (version && inForce === null) {
|
|
36312
|
+
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
|
|
35227
36313
|
...payload.files ?? {},
|
|
35228
36314
|
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
35229
36315
|
`
|
|
@@ -35235,12 +36321,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35235
36321
|
if (payload.legacy_memory_deletes?.length) {
|
|
35236
36322
|
deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
|
|
35237
36323
|
}
|
|
35238
|
-
|
|
36324
|
+
const legacyDecisions = inForce !== null ? this.boundLegacyDecisions : payload.legacy_memory_fact_decisions;
|
|
36325
|
+
if (legacyDecisions?.length) {
|
|
35239
36326
|
const provider = this.cfg.memoryProvider ?? "securecoms";
|
|
35240
36327
|
const result = await applyLegacyMemoryFactDecisions(
|
|
35241
36328
|
this.home,
|
|
35242
36329
|
this.cfg.identityId,
|
|
35243
|
-
|
|
36330
|
+
legacyDecisions,
|
|
35244
36331
|
async (channelId, fact, assignmentKey) => {
|
|
35245
36332
|
if (provider === "none" || provider === "hermes") {
|
|
35246
36333
|
throw new Error("configured memory provider has no protected channel-fact import path");
|
|
@@ -35259,72 +36346,177 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35259
36346
|
});
|
|
35260
36347
|
}
|
|
35261
36348
|
}
|
|
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");
|
|
36349
|
+
await this.postRuntimeConfigReport(base, version, skillsJson);
|
|
35324
36350
|
} catch (e) {
|
|
35325
36351
|
writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
|
|
35326
36352
|
}
|
|
35327
36353
|
}
|
|
36354
|
+
/**
|
|
36355
|
+
* Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
|
|
36356
|
+
* after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
|
|
36357
|
+
* the binding left null.
|
|
36358
|
+
*/
|
|
36359
|
+
async applyUnderManifest(manifest, pinKeyB64, payload, version) {
|
|
36360
|
+
let prior;
|
|
36361
|
+
try {
|
|
36362
|
+
prior = (await this.store.loadToolGrantApplied(pinKeyB64))?.search;
|
|
36363
|
+
} catch (e) {
|
|
36364
|
+
prior = void 0;
|
|
36365
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_read_failed", ["applied_read_failed"]) }, e);
|
|
36366
|
+
}
|
|
36367
|
+
let bound;
|
|
36368
|
+
try {
|
|
36369
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
|
|
36370
|
+
} catch (e) {
|
|
36371
|
+
bound = null;
|
|
36372
|
+
return this.bindFailed(manifest, payload.memory, e);
|
|
36373
|
+
}
|
|
36374
|
+
const counter = manifest.counter;
|
|
36375
|
+
const state = bound.mismatches.length === 0 ? "bound" : "mismatch";
|
|
36376
|
+
const old = this.manifestBinding;
|
|
36377
|
+
if (!(old !== null && old.state === "bound" && old.counter === counter && state === "bound")) this.manifestBinding = null;
|
|
36378
|
+
this.configBindingMismatches = bound.mismatches;
|
|
36379
|
+
this.boundLegacyDecisions = bound.legacyDecisions;
|
|
36380
|
+
this.logConfigBinding(bound);
|
|
36381
|
+
this.targetMismatches = { endpoint: bound.targetMismatches.endpoint, database: bound.targetMismatches.database };
|
|
36382
|
+
await this.applyPayloadMemory(bound.memory);
|
|
36383
|
+
if (!version) {
|
|
36384
|
+
this.manifestBinding = null;
|
|
36385
|
+
return true;
|
|
36386
|
+
}
|
|
36387
|
+
try {
|
|
36388
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
|
|
36389
|
+
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
36390
|
+
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
36391
|
+
} catch (e) {
|
|
36392
|
+
this.manifestBinding = null;
|
|
36393
|
+
throw e;
|
|
36394
|
+
}
|
|
36395
|
+
this.manifestBinding = { counter, state, signedTargets: { ...bound.signedTargets } };
|
|
36396
|
+
if (bound.appliedSearch !== void 0) {
|
|
36397
|
+
try {
|
|
36398
|
+
await this.store.saveToolGrantApplied(pinKeyB64, { counter, search: bound.appliedSearch });
|
|
36399
|
+
} catch (e) {
|
|
36400
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_save_failed", ["applied_save_failed"]) }, e);
|
|
36401
|
+
}
|
|
36402
|
+
}
|
|
36403
|
+
return true;
|
|
36404
|
+
}
|
|
36405
|
+
/** Spec §7.6 (plan 4 Task 15): content-free. One line per mismatched field, one per dropped target. */
|
|
36406
|
+
logConfigBinding(bound) {
|
|
36407
|
+
if (bound.mismatches.length === 0) {
|
|
36408
|
+
writeServiceLog2("info", "authz.config_binding", { state: ServiceLogEnum.of("ok", ["ok"]) });
|
|
36409
|
+
}
|
|
36410
|
+
for (const field of bound.mismatches) {
|
|
36411
|
+
writeServiceLog2("warn", "authz.config_binding", {
|
|
36412
|
+
state: ServiceLogEnum.of("mismatch", ["mismatch"]),
|
|
36413
|
+
field: ServiceLogEnum.of(field, ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"])
|
|
36414
|
+
});
|
|
36415
|
+
}
|
|
36416
|
+
for (const kind2 of ["endpoint", "database"]) {
|
|
36417
|
+
for (let i = 0; i < bound.targetMismatches[kind2]; i++) {
|
|
36418
|
+
writeServiceLog2("warn", "authz.target_mismatch", { kind: ServiceLogEnum.of(kind2, ["endpoint", "database"]) });
|
|
36419
|
+
}
|
|
36420
|
+
}
|
|
36421
|
+
}
|
|
36422
|
+
/** H-4: the binding could not be computed. The memory still applies, with the signed mode; nothing else is written. */
|
|
36423
|
+
async bindFailed(manifest, rawMemory, e) {
|
|
36424
|
+
this.manifestBinding = null;
|
|
36425
|
+
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
36426
|
+
const signed = { mode: manifest.config.memory.mode, ...manifest.config.memory.recall_days !== void 0 ? { recall_days: manifest.config.memory.recall_days } : {} };
|
|
36427
|
+
let memory = signed;
|
|
36428
|
+
if (rawMemory !== null && typeof rawMemory === "object" && !Array.isArray(rawMemory)) {
|
|
36429
|
+
const { mode: _m, recall_days: _r, ...rest } = rawMemory;
|
|
36430
|
+
memory = { ...signed, ...rest };
|
|
36431
|
+
}
|
|
36432
|
+
await this.applyPayloadMemory(memory);
|
|
36433
|
+
return false;
|
|
36434
|
+
}
|
|
36435
|
+
/** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
|
|
36436
|
+
overwriteStalePolicy() {
|
|
36437
|
+
if (!existsSync5(join17(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
|
|
36438
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
|
|
36439
|
+
}
|
|
36440
|
+
/** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
|
|
36441
|
+
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
36442
|
+
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
36443
|
+
const report = {
|
|
36444
|
+
applied_version: version,
|
|
36445
|
+
paths: checkConfiguredPaths(skillsJson),
|
|
36446
|
+
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
36447
|
+
// the file as it now stands, not the one they just pruned from.
|
|
36448
|
+
memory: readManagedMemory(this.home, this.cfg.identityId),
|
|
36449
|
+
// E13: pass through what the runtime reported. The daemon does not
|
|
36450
|
+
// interpret any of it.
|
|
36451
|
+
local_persona: local?.persona ?? null,
|
|
36452
|
+
// Only the runtime knows whether a search key file is configured — it
|
|
36453
|
+
// is an environment value on that process, invisible to the daemon and
|
|
36454
|
+
// to the backend. The backend needs it to generate a truthful tool
|
|
36455
|
+
// paragraph, so it rides along here.
|
|
36456
|
+
has_search_key: local?.has_search_key ?? false,
|
|
36457
|
+
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
36458
|
+
// whether one is configured in its own environment.
|
|
36459
|
+
has_llm_key: local?.has_llm_key ?? false,
|
|
36460
|
+
// The backend uses this only to expose target channels compatible with
|
|
36461
|
+
// the actual protected-memory adapter. It is an enum, never a path or
|
|
36462
|
+
// provider credential.
|
|
36463
|
+
memory_provider: this.cfg.memoryProvider ?? "securecoms",
|
|
36464
|
+
// Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
|
|
36465
|
+
// unchanged. memory_notes is true only when notes are actually on (the
|
|
36466
|
+
// provider is securecoms and the parsed payload.memory keeps them on);
|
|
36467
|
+
// the generation is the store's applied value, never the payload's.
|
|
36468
|
+
// Final review I-1: and only once a notes-reading runtime said hello.
|
|
36469
|
+
memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
|
|
36470
|
+
// Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
|
|
36471
|
+
// 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
|
|
36472
|
+
// booleans only (U1).
|
|
36473
|
+
...this.reportedUnified(),
|
|
36474
|
+
...this.reportedClearGeneration(),
|
|
36475
|
+
// 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
|
|
36476
|
+
...this.reportedRecall(),
|
|
36477
|
+
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
36478
|
+
// regular-file bytes and strips every local path before this leaves the
|
|
36479
|
+
// machine.
|
|
36480
|
+
legacy_memory: local?.legacy_memory ?? [],
|
|
36481
|
+
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
36482
|
+
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
36483
|
+
// one enum only; readLocalReport rebuilds it so no path rides along.
|
|
36484
|
+
secret_paths: local?.secret_paths ?? null,
|
|
36485
|
+
// Owner-signed tool grants (spec §7.7, D-6): enums and numbers only. It
|
|
36486
|
+
// authorizes nothing and a compromised backend can forge what it shows
|
|
36487
|
+
// (residual 9). The backend drops it until Plan 2 or 3 stores it.
|
|
36488
|
+
tool_grant_status: {
|
|
36489
|
+
pin: this.cfg.ownerGrantPin?.state ?? "no_pin",
|
|
36490
|
+
manifest: this.manifestState,
|
|
36491
|
+
applied_counter: this.toolGrantCounter,
|
|
36492
|
+
owner_dm_verify_failures: this.ownerVerifyFailures,
|
|
36493
|
+
// Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
|
|
36494
|
+
config_binding_mismatches: [...this.configBindingMismatches],
|
|
36495
|
+
target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
|
|
36496
|
+
capability: this.lastConsumerCapability
|
|
36497
|
+
}
|
|
36498
|
+
};
|
|
36499
|
+
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
36500
|
+
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
36501
|
+
method: "POST",
|
|
36502
|
+
headers: {
|
|
36503
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
36504
|
+
"content-type": "application/json"
|
|
36505
|
+
},
|
|
36506
|
+
body: JSON.stringify(report)
|
|
36507
|
+
});
|
|
36508
|
+
if (!posted.ok) {
|
|
36509
|
+
if (version) {
|
|
36510
|
+
writeServiceLog2("warn", "runtime_config.report_failed", { status: posted.status, version });
|
|
36511
|
+
} else {
|
|
36512
|
+
writeServiceLog2("warn", "runtime_config.legacy_memory_report_failed", { status: posted.status });
|
|
36513
|
+
}
|
|
36514
|
+
return;
|
|
36515
|
+
}
|
|
36516
|
+
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
36517
|
+
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
36518
|
+
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
36519
|
+
}
|
|
35328
36520
|
/**
|
|
35329
36521
|
* Enforce the inbound-buffer cap with a DROP-OLDEST policy.
|
|
35330
36522
|
*
|
|
@@ -35347,6 +36539,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35347
36539
|
async start() {
|
|
35348
36540
|
this.startedAt = Date.now();
|
|
35349
36541
|
await this.restoreRuntimeInbox();
|
|
36542
|
+
await this.loadToolGrantState();
|
|
36543
|
+
try {
|
|
36544
|
+
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
36545
|
+
} catch (e) {
|
|
36546
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
36547
|
+
}
|
|
35350
36548
|
try {
|
|
35351
36549
|
this.store.pruneRecall();
|
|
35352
36550
|
this.store.reconcileRecallClearGeneration();
|
|
@@ -36480,8 +37678,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36480
37678
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
36481
37679
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
36482
37680
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
36483
|
-
if (
|
|
36484
|
-
|
|
37681
|
+
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
37682
|
+
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";
|
|
37683
|
+
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
37684
|
+
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
36485
37685
|
}
|
|
36486
37686
|
/**
|
|
36487
37687
|
* Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
|
|
@@ -36496,8 +37696,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36496
37696
|
return { ...this.store.itemCapture(channelId), clearGeneration: stamp };
|
|
36497
37697
|
}
|
|
36498
37698
|
/** 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) {
|
|
37699
|
+
spawnUnifiedReply(channelId, text, messageId, expect, origin) {
|
|
36500
37700
|
if (expect === null) return;
|
|
37701
|
+
if (origin === "fast_path") return;
|
|
36501
37702
|
const work = (async () => {
|
|
36502
37703
|
try {
|
|
36503
37704
|
if (!this.unifiedProviderOn()) return;
|
|
@@ -36513,7 +37714,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36513
37714
|
source: "agent_reply",
|
|
36514
37715
|
trust: "reference",
|
|
36515
37716
|
text,
|
|
36516
|
-
expect
|
|
37717
|
+
expect,
|
|
37718
|
+
role: "agent",
|
|
37719
|
+
origin
|
|
36517
37720
|
});
|
|
36518
37721
|
} catch {
|
|
36519
37722
|
writeServiceLog2("warn", "memory.unified_reply_failed", {});
|
|
@@ -37239,6 +38442,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37239
38442
|
);
|
|
37240
38443
|
return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
|
|
37241
38444
|
}
|
|
38445
|
+
/**
|
|
38446
|
+
* Decision D-9 (audit H-3): a turn that failed owner verification gets only
|
|
38447
|
+
* its own text, never the prior-turn history snapshot, for every provider.
|
|
38448
|
+
*/
|
|
38449
|
+
mayReadHistoryForTurn(messageId, authorization) {
|
|
38450
|
+
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38451
|
+
}
|
|
37242
38452
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
37243
38453
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
37244
38454
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -37248,16 +38458,119 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37248
38458
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
37249
38459
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
37250
38460
|
*/
|
|
37251
|
-
async memoryForTurn(channelId, authorization, limit = 50) {
|
|
37252
|
-
if (!this.
|
|
38461
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
38462
|
+
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
37253
38463
|
return { recent: [], facts: [] };
|
|
37254
38464
|
}
|
|
37255
38465
|
try {
|
|
37256
|
-
return await this.memory.context(channelId, limit);
|
|
38466
|
+
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
37257
38467
|
} catch {
|
|
37258
38468
|
return { recent: [], facts: [] };
|
|
37259
38469
|
}
|
|
37260
38470
|
}
|
|
38471
|
+
/**
|
|
38472
|
+
* 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
|
|
38473
|
+
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
38474
|
+
*/
|
|
38475
|
+
voiceFenceOn() {
|
|
38476
|
+
return this.cfg.ownerGrantPin?.pin != null;
|
|
38477
|
+
}
|
|
38478
|
+
/** The keyed and legacy fast-reply ids of every peer row of the conversation (audit r2 M-1; never the 50-row window). */
|
|
38479
|
+
fenceContext(channelId) {
|
|
38480
|
+
let ids;
|
|
38481
|
+
const build = () => {
|
|
38482
|
+
const out = /* @__PURE__ */ new Set();
|
|
38483
|
+
for (const p of this.store.memoryPeerMessageIds(channelId)) {
|
|
38484
|
+
out.add(fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, p));
|
|
38485
|
+
out.add(legacyFastReplyId(p));
|
|
38486
|
+
}
|
|
38487
|
+
return out;
|
|
38488
|
+
};
|
|
38489
|
+
return { get fastIds() {
|
|
38490
|
+
return ids ??= build();
|
|
38491
|
+
} };
|
|
38492
|
+
}
|
|
38493
|
+
/**
|
|
38494
|
+
* Drop agent lines classified fast_path, untrusted or unknown from a turn's memory (every turn of a pinned agent).
|
|
38495
|
+
* `strict` (review H-3, a turn that holds a manifest grant): relay-class peer lines go too (sender not the pinned
|
|
38496
|
+
* owner, or voice modality). Otherwise peer lines are never dropped.
|
|
38497
|
+
*/
|
|
38498
|
+
fenceMemory(channelId, ctx, manifestTurn = false) {
|
|
38499
|
+
if (!this.voiceFenceOn()) return ctx;
|
|
38500
|
+
const scoped = this.taintScope(channelId);
|
|
38501
|
+
const strict = manifestTurn && scoped;
|
|
38502
|
+
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38503
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38504
|
+
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38505
|
+
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38506
|
+
const fence = this.fenceContext(channelId);
|
|
38507
|
+
const recent = ctx.recent.filter((t) => {
|
|
38508
|
+
const m = meta.get(t.seq);
|
|
38509
|
+
if (t.role !== "agent") return !(strict && isRelayClassPeer({ peerId: t.peerId, modality: m?.modality }, ownerId));
|
|
38510
|
+
const origin = classifyAgentLine({ ...m ?? {}, ts: t.ts }, fence);
|
|
38511
|
+
return origin === "reply" || origin === "untrusted" && !scoped;
|
|
38512
|
+
});
|
|
38513
|
+
return recent.length === ctx.recent.length ? ctx : { ...ctx, recent };
|
|
38514
|
+
}
|
|
38515
|
+
/**
|
|
38516
|
+
* Review H-3: true when a delivered frame holds a line a manifest turn would exclude (a relay-class peer line; agent
|
|
38517
|
+
* lines in a frame are already fenced). Such a turn's reply is recorded "untrusted".
|
|
38518
|
+
*/
|
|
38519
|
+
frameHoldsRelayClass(m) {
|
|
38520
|
+
const recent = m.memory?.recent ?? [];
|
|
38521
|
+
if (recent.length === 0) return false;
|
|
38522
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38523
|
+
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38524
|
+
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38525
|
+
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
38526
|
+
}
|
|
38527
|
+
/**
|
|
38528
|
+
* Review H-3 attribution: message ids of turns delivered on a pinned agent whose frame held no relay-class line.
|
|
38529
|
+
* A reply is recorded "reply" only when its in-reply-to is here; anything else (tainted, unknown after a restart,
|
|
38530
|
+
* no in-reply-to) is "untrusted". Bounded; the oldest are forgotten first (forgetting only makes a reply untrusted).
|
|
38531
|
+
*/
|
|
38532
|
+
cleanTurns = /* @__PURE__ */ new Set();
|
|
38533
|
+
noteDeliveredFrame(m, unifiedTainted = false) {
|
|
38534
|
+
if (!this.taintScope(m.channel_id)) return;
|
|
38535
|
+
const tainted = unifiedTainted || this.turnIsRelayClass(m) || !holdsManifestGrant(m.authorization) && this.frameHoldsRelayClass(m);
|
|
38536
|
+
if (tainted) {
|
|
38537
|
+
this.cleanTurns.delete(m.message_id);
|
|
38538
|
+
this.taintedTurns.add(m.message_id);
|
|
38539
|
+
if (this.taintedTurns.size > 5e3) this.taintedTurns.delete(this.taintedTurns.values().next().value);
|
|
38540
|
+
return;
|
|
38541
|
+
}
|
|
38542
|
+
if (this.taintedTurns.has(m.message_id)) return;
|
|
38543
|
+
this.cleanTurns.add(m.message_id);
|
|
38544
|
+
if (this.cleanTurns.size > 5e3) this.cleanTurns.delete(this.cleanTurns.values().next().value);
|
|
38545
|
+
}
|
|
38546
|
+
taintedTurns = /* @__PURE__ */ new Set();
|
|
38547
|
+
/**
|
|
38548
|
+
* 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
|
|
38549
|
+
* frame in the owner DM (cleanTurns, not taintedTurns), so a tainted fallback turn cannot plant a note that a later
|
|
38550
|
+
* manifest turn reads. Taint is noted at delivery, before any note frame for the turn can arrive.
|
|
38551
|
+
*/
|
|
38552
|
+
noteTurnClean(messageId) {
|
|
38553
|
+
return !this.voiceFenceOn() || this.cleanTurns.has(messageId) && !this.taintedTurns.has(messageId);
|
|
38554
|
+
}
|
|
38555
|
+
/**
|
|
38556
|
+
* Review R2-H1: the turn itself is relay-class (its frame memory is prior-turn only, so its own sender is never in it):
|
|
38557
|
+
* a sender that is not the pinned owner, a relay speaker claim, or a voice turn.
|
|
38558
|
+
*/
|
|
38559
|
+
turnIsRelayClass(m) {
|
|
38560
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
|
|
38561
|
+
return m.sender_id !== ownerId || m.authorization?.relay_speaker !== void 0 || m.voice === true || m.transcribed === true || m.speaker !== void 0;
|
|
38562
|
+
}
|
|
38563
|
+
/**
|
|
38564
|
+
* R2-M1 ruling: the channel where the H-3 taint and the `untrusted` exclusion apply, on a pinned agent: the signed
|
|
38565
|
+
* owner_dm_channel_id of the manifest in force. Before any manifest is held there is no signed DM, so every channel
|
|
38566
|
+
* not known to be a room counts (fail closed: a reply recorded there before the first manifest must not reach it).
|
|
38567
|
+
*/
|
|
38568
|
+
taintScope(channelId) {
|
|
38569
|
+
if (!this.voiceFenceOn()) return false;
|
|
38570
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
38571
|
+
if (signedDm !== void 0) return signedDm === channelId;
|
|
38572
|
+
return (this.freshMeta(channelId)?.is_dm ?? this.store.getChannelMeta(channelId)?.isDm) !== false;
|
|
38573
|
+
}
|
|
37261
38574
|
/**
|
|
37262
38575
|
* Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
|
|
37263
38576
|
* value turns notes off and deletes nothing. A readable one goes through
|
|
@@ -37713,10 +39026,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37713
39026
|
if (isDm !== true || ownerId === void 0) return { kind: "unknown" };
|
|
37714
39027
|
return this.store.recordDmOwner(c, ownerId) ? { kind: "owner", ownerId } : { kind: "unknown" };
|
|
37715
39028
|
};
|
|
39029
|
+
const fences = /* @__PURE__ */ new Map();
|
|
39030
|
+
const classifyHistoryOrigin = (c, r) => {
|
|
39031
|
+
const f = fences.get(c) ?? (fences.set(c, this.fenceContext(c)), fences.get(c));
|
|
39032
|
+
return classifyAgentLine({ origin: r.origin, modality: r.modality, ...r.messageId ? { messageId: r.messageId } : {}, ts: r.recordedAt }, f);
|
|
39033
|
+
};
|
|
37716
39034
|
const out = await runMigration({
|
|
37717
39035
|
store: this.store,
|
|
37718
39036
|
items: this.items,
|
|
37719
39037
|
classify,
|
|
39038
|
+
classifyHistoryOrigin,
|
|
37720
39039
|
stopping: () => this.closingDown,
|
|
37721
39040
|
pinsChanged: () => this.notes.exclusive(async () => {
|
|
37722
39041
|
this.unifiedReads?.bumpGeneration();
|
|
@@ -37736,7 +39055,24 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37736
39055
|
const a = m.authorization;
|
|
37737
39056
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
37738
39057
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
37739
|
-
|
|
39058
|
+
if (scopeId === null) return void 0;
|
|
39059
|
+
if (!this.voiceFenceOn()) return this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId);
|
|
39060
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
39061
|
+
const ctxFor = (c) => contexts.get(c) ?? (contexts.set(c, this.fenceContext(c)), contexts.get(c));
|
|
39062
|
+
const trace = (c, at) => this.store.memoryRowForTrace(c, at);
|
|
39063
|
+
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39064
|
+
const scoped = this.taintScope(m.channel_id);
|
|
39065
|
+
const strict = scoped && holdsManifestGrant(m.authorization);
|
|
39066
|
+
const strictRule = { ownerId, turnChannelId: m.channel_id };
|
|
39067
|
+
let tainted = false;
|
|
39068
|
+
const out = this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId, (i) => {
|
|
39069
|
+
const facts = { channelId: i.channelId, source: i.source, originId: i.originId, role: i.role, origin: i.origin, sourceRef: i.sourceRef };
|
|
39070
|
+
const keep = classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strict ? strictRule : void 0, !scoped) === "keep";
|
|
39071
|
+
if (keep && scoped && !strict && classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strictRule) === "exclude") tainted = true;
|
|
39072
|
+
return keep;
|
|
39073
|
+
});
|
|
39074
|
+
if (tainted) this.noteDeliveredFrame(m, true);
|
|
39075
|
+
return out;
|
|
37740
39076
|
}
|
|
37741
39077
|
/** ws-server: the hello echoes memory_unified only with the part 1 switch compiled on. */
|
|
37742
39078
|
unifiedFeatureOn() {
|
|
@@ -38038,6 +39374,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38038
39374
|
if (rec === void 0) return refuse("message");
|
|
38039
39375
|
const channelId = rec.channel_id;
|
|
38040
39376
|
if (rec.authorization?.memory_write !== true) return refuse("audience");
|
|
39377
|
+
if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
|
|
38041
39378
|
const held = await this.heldPosition(channelId);
|
|
38042
39379
|
const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
|
|
38043
39380
|
if (now.memory_write !== true) return refuse("audience");
|
|
@@ -38141,6 +39478,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38141
39478
|
if (rec === void 0) return refuse("message");
|
|
38142
39479
|
const channelId = rec.channel_id;
|
|
38143
39480
|
if (rec.authorization?.memory_write !== true) return refuse("audience");
|
|
39481
|
+
if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
|
|
38144
39482
|
const held = await this.heldPosition(channelId);
|
|
38145
39483
|
if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse("audience");
|
|
38146
39484
|
const scopeId = this.ownerScopeNow(channelId);
|
|
@@ -38224,6 +39562,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38224
39562
|
return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
|
|
38225
39563
|
}
|
|
38226
39564
|
async memoryContext(conversationId, limit) {
|
|
39565
|
+
for (const [id, channelId] of this.ownerVerifyFailedIds) {
|
|
39566
|
+
if (channelId === conversationId && this.runtimePendingIds.has(id)) return { recent: [], facts: [] };
|
|
39567
|
+
}
|
|
38227
39568
|
if (!await this.mayReadOpenClawMemory(conversationId)) {
|
|
38228
39569
|
return { recent: [], facts: [] };
|
|
38229
39570
|
}
|
|
@@ -38371,7 +39712,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38371
39712
|
const fastLlm = this.fastLlm;
|
|
38372
39713
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
38373
39714
|
let memory;
|
|
38374
|
-
if (!this.
|
|
39715
|
+
if (!this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
38375
39716
|
memory = { recent: [], facts: [] };
|
|
38376
39717
|
} else {
|
|
38377
39718
|
try {
|
|
@@ -38419,15 +39760,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38419
39760
|
* frame (the "fast path failed" semantics), so a crash mid-call never loses
|
|
38420
39761
|
* the turn.
|
|
38421
39762
|
*/
|
|
38422
|
-
async stageHeldRuntimeMessage(
|
|
38423
|
-
if (this.runtimePendingIds.has(
|
|
39763
|
+
async stageHeldRuntimeMessage(held, opts = {}) {
|
|
39764
|
+
if (this.runtimePendingIds.has(held.message_id)) return true;
|
|
38424
39765
|
if (this.runtimePendingIds.size >= this.maxBuffer) {
|
|
38425
39766
|
writeServiceLog2("warn", "runtime_inbox.full");
|
|
38426
39767
|
return false;
|
|
38427
39768
|
}
|
|
39769
|
+
const message = opts.absorbed ? held : this.applyRoomSearchBackstop(held);
|
|
38428
39770
|
try {
|
|
38429
39771
|
await this.stageSealed(message);
|
|
38430
39772
|
this.runtimePendingIds.add(message.message_id);
|
|
39773
|
+
if (message !== held) this.backstopStripped.add(message.message_id);
|
|
38431
39774
|
return true;
|
|
38432
39775
|
} catch (error) {
|
|
38433
39776
|
writeServiceLog2("error", "runtime_inbox.stage_failed", {}, error);
|
|
@@ -38443,7 +39786,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38443
39786
|
}
|
|
38444
39787
|
const memory = await this.memoryAtRelease(message);
|
|
38445
39788
|
if (this.closingDown || !this.runtimePendingIds.has(message.message_id)) return;
|
|
38446
|
-
this.
|
|
39789
|
+
const out = this.backstopStripped.delete(message.message_id) ? this.withoutWebSearch(message) : message;
|
|
39790
|
+
this.buffer.push(memory === message.memory ? out : { ...out, memory });
|
|
38447
39791
|
this.wakeListeners();
|
|
38448
39792
|
}
|
|
38449
39793
|
/**
|
|
@@ -38461,7 +39805,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38461
39805
|
*/
|
|
38462
39806
|
async memoryAtRelease(message) {
|
|
38463
39807
|
const prior = message.memory;
|
|
38464
|
-
if (!prior || !this.
|
|
39808
|
+
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
38465
39809
|
let fresh;
|
|
38466
39810
|
try {
|
|
38467
39811
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -38469,7 +39813,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38469
39813
|
return prior;
|
|
38470
39814
|
}
|
|
38471
39815
|
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);
|
|
39816
|
+
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
39817
|
if (spoken.length === 0) return prior;
|
|
38474
39818
|
return {
|
|
38475
39819
|
recent: [...prior.recent, ...spoken].slice(-_BridgeAgent.TURN_MEMORY_LIMIT),
|
|
@@ -38752,6 +40096,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38752
40096
|
});
|
|
38753
40097
|
}
|
|
38754
40098
|
this.channelRoster.set(channelId, { members, at: Date.now(), epoch, groupId, crossOrg: roomIsCrossOrg(rows) });
|
|
40099
|
+
const grantPin = this.cfg.ownerGrantPin?.pin;
|
|
40100
|
+
if (grantPin && !this.serverOwnerMismatchLogged && liveRows.some((r) => r.is_agent_owner === true && !r.is_agent && r.identity_id !== grantPin.ownerIdentityId)) {
|
|
40101
|
+
this.serverOwnerMismatchLogged = true;
|
|
40102
|
+
writeServiceLog2("warn", "authz.pin", { state: ServiceLogEnum.of("server_owner_mismatch", ["server_owner_mismatch"]) });
|
|
40103
|
+
}
|
|
38755
40104
|
const self = members.get(this.cfg.identityId);
|
|
38756
40105
|
if (self?.name) this.lastOwnDisplayName = self.name;
|
|
38757
40106
|
await this.resolveAndPinOwner(channelId, liveRows);
|
|
@@ -38778,11 +40127,200 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38778
40127
|
}
|
|
38779
40128
|
}
|
|
38780
40129
|
}
|
|
40130
|
+
/**
|
|
40131
|
+
* Owner-signed tool grants, step 1 (spec §7.2; decisions D-1, D-2, D-4, D-8,
|
|
40132
|
+
* D-9; audits H-1, M-2, M-4, E-1..E-3). The verdict for one delivered turn.
|
|
40133
|
+
*
|
|
40134
|
+
* `unchecked` when no pin is loaded or the sender is not the pinned owner.
|
|
40135
|
+
* Otherwise, in order, the first failure wins: the identity signature over the
|
|
40136
|
+
* raw payload bytes with the pinned key; the verified bytes must be the
|
|
40137
|
+
* decrypted envelope; in a DM only (D-1), the signed DM channel and the MLS
|
|
40138
|
+
* leaf set; then the replay set and closed epochs, keyed by the agent's OWN
|
|
40139
|
+
* held group (never the backend's row label, M-2). NEVER throws (M-4): any
|
|
40140
|
+
* exception is `failed verify_error`, and the row is acked like any other turn.
|
|
40141
|
+
*/
|
|
40142
|
+
async verifyOwnerTurn(m) {
|
|
40143
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40144
|
+
if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
|
|
40145
|
+
try {
|
|
40146
|
+
const sig = verifyOwnerSignature({ payloadHex: m.payloadHex, identitySignature: m.identitySignature }, pin);
|
|
40147
|
+
if (sig.state !== "ok") return this.ownerTurnFailed(m, this.isAckedOwnerOriginal(m) ? "replay" : sig.state);
|
|
40148
|
+
const v = sig.envelope;
|
|
40149
|
+
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) {
|
|
40150
|
+
return this.ownerTurnFailed(m, "malformed");
|
|
40151
|
+
}
|
|
40152
|
+
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
40153
|
+
let leafClass = "ok";
|
|
40154
|
+
if (this.freshMeta(m.channelId)?.is_dm === true) {
|
|
40155
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
40156
|
+
if (signedDm !== void 0 && signedDm !== m.channelId) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
40157
|
+
leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
|
|
40158
|
+
ownerIdentityId: pin.ownerIdentityId,
|
|
40159
|
+
agentIdentityId: this.cfg.identityId,
|
|
40160
|
+
relayIdentityId: this.toolGrantManifest?.owner_dm_relay_identity_id,
|
|
40161
|
+
isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
|
|
40162
|
+
});
|
|
40163
|
+
if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
|
|
40164
|
+
}
|
|
40165
|
+
const held = await this.heldPosition(m.channelId);
|
|
40166
|
+
if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
|
|
40167
|
+
if (!this.runtimePendingIds.has(m.messageId) && (this.store.hasOwnerMessage(m.channelId, m.messageId) || this.store.isOwnerEpochClosed(m.channelId, held.groupId, v.mls_group_epoch))) {
|
|
40168
|
+
return this.ownerTurnFailed(m, "replay");
|
|
40169
|
+
}
|
|
40170
|
+
const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
|
|
40171
|
+
this.ownerReplayPending.set(m.messageId, replay);
|
|
40172
|
+
return Object.freeze({ kind: leafClass === "unsigned_relay" ? "constants_only" : "verified", replay });
|
|
40173
|
+
} catch {
|
|
40174
|
+
return this.ownerTurnFailed(m, "verify_error");
|
|
40175
|
+
}
|
|
40176
|
+
}
|
|
40177
|
+
/** Log, count and (except a replay, audit E-3) record one failed owner-turn check. Never throws (audit E-2). */
|
|
40178
|
+
ownerTurnFailed(m, reason) {
|
|
40179
|
+
writeServiceLog2("warn", "authz.owner_dm_verify", {
|
|
40180
|
+
state: ServiceLogEnum.of(reason, [
|
|
40181
|
+
"bad_signature",
|
|
40182
|
+
"malformed",
|
|
40183
|
+
"unknown_algo",
|
|
40184
|
+
"no_signature",
|
|
40185
|
+
"not_owner_dm_leaves",
|
|
40186
|
+
"wrong_dm_channel",
|
|
40187
|
+
"replay",
|
|
40188
|
+
"verify_error"
|
|
40189
|
+
])
|
|
40190
|
+
});
|
|
40191
|
+
this.ownerVerifyFailures += 1;
|
|
40192
|
+
if (reason !== "replay" && !this.isVerifiedOwnerOriginal(m)) {
|
|
40193
|
+
this.ownerVerifyFailedIds.set(m.messageId, m.channelId);
|
|
40194
|
+
try {
|
|
40195
|
+
this.store.recordOwnerVerifyFailure(m.channelId, m.messageId, Date.now());
|
|
40196
|
+
} catch {
|
|
40197
|
+
writeServiceLog2("warn", "authz.owner_verify_record_failed");
|
|
40198
|
+
}
|
|
40199
|
+
}
|
|
40200
|
+
return Object.freeze({ kind: "failed", reason });
|
|
40201
|
+
}
|
|
40202
|
+
/** Review M-A: a verified original of this id was staged and is still pending, or was recorded in the replay set. */
|
|
40203
|
+
isVerifiedOwnerOriginal(m) {
|
|
40204
|
+
return this.runtimePendingIds.has(m.messageId) || this.isAckedOwnerOriginal(m);
|
|
40205
|
+
}
|
|
40206
|
+
/** Review M-A: a verified original of this id was staged and acked (it is in the replay set, no longer pending). */
|
|
40207
|
+
isAckedOwnerOriginal(m) {
|
|
40208
|
+
if (this.runtimePendingIds.has(m.messageId)) return false;
|
|
40209
|
+
try {
|
|
40210
|
+
return this.store.hasOwnerMessage(m.channelId, m.messageId);
|
|
40211
|
+
} catch {
|
|
40212
|
+
return false;
|
|
40213
|
+
}
|
|
40214
|
+
}
|
|
40215
|
+
/** D-9: memory first, then the store (a restart before the reply). Unpinned: never. A store read that throws withholds (fail closed). */
|
|
40216
|
+
isOwnerVerifyFailed(messageId) {
|
|
40217
|
+
if (this.ownerVerifyFailedIds.has(messageId)) return true;
|
|
40218
|
+
if (!this.cfg.ownerGrantPin?.pin) return false;
|
|
40219
|
+
try {
|
|
40220
|
+
return this.store.isOwnerVerifyFailed(messageId);
|
|
40221
|
+
} catch {
|
|
40222
|
+
return true;
|
|
40223
|
+
}
|
|
40224
|
+
}
|
|
40225
|
+
/**
|
|
40226
|
+
* Decision D-2, audit L-2: prune the owner replay set at most once per
|
|
40227
|
+
* OWNER_REPLAY_PRUNE_INTERVAL_MS, keeping each channel's current (group,
|
|
40228
|
+
* epoch). A channel whose held position is unknown is left out (not pruned).
|
|
40229
|
+
*/
|
|
40230
|
+
async maybePruneOwnerReplay() {
|
|
40231
|
+
if (!this.cfg.ownerGrantPin?.pin) return;
|
|
40232
|
+
const now = this.monoNow();
|
|
40233
|
+
if (now - this.lastOwnerReplayPruneAt < OWNER_REPLAY_PRUNE_INTERVAL_MS) return;
|
|
40234
|
+
this.lastOwnerReplayPruneAt = now;
|
|
40235
|
+
try {
|
|
40236
|
+
const currentOf = /* @__PURE__ */ new Map();
|
|
40237
|
+
for (const channelId of this.store.listOwnerReplayChannels()) {
|
|
40238
|
+
const held = await this.heldPosition(channelId);
|
|
40239
|
+
if (held.groupId !== null && held.epoch !== null) currentOf.set(channelId, { groupId: held.groupId, epoch: held.epoch });
|
|
40240
|
+
}
|
|
40241
|
+
this.store.pruneOwnerReplay(Date.now(), currentOf);
|
|
40242
|
+
} catch (e) {
|
|
40243
|
+
writeServiceLog2("warn", "authz.replay_prune_failed", {}, e);
|
|
40244
|
+
}
|
|
40245
|
+
}
|
|
40246
|
+
/** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
|
|
40247
|
+
async loadToolGrantState() {
|
|
40248
|
+
const pin = this.cfg.ownerGrantPin?.pin;
|
|
40249
|
+
if (!pin) return;
|
|
40250
|
+
try {
|
|
40251
|
+
this.store.dropToolGrantManifestsExcept(pin.publicKeyB64);
|
|
40252
|
+
this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
|
|
40253
|
+
const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
|
|
40254
|
+
if (saved) {
|
|
40255
|
+
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
|
|
40256
|
+
if (v.state === "ok") {
|
|
40257
|
+
this.toolGrantManifest = v.manifest;
|
|
40258
|
+
this.toolGrantCounter = v.manifest.counter;
|
|
40259
|
+
} else {
|
|
40260
|
+
this.logManifest("malformed");
|
|
40261
|
+
}
|
|
40262
|
+
}
|
|
40263
|
+
} catch (e) {
|
|
40264
|
+
writeServiceLog2("warn", "authz.manifest_load_failed", {}, e);
|
|
40265
|
+
}
|
|
40266
|
+
await this.maybePruneOwnerReplay();
|
|
40267
|
+
}
|
|
40268
|
+
/**
|
|
40269
|
+
* Spec §7.1: verify and record a served manifest. Never writes a file and
|
|
40270
|
+
* never changes a grant in step 1. A save that throws keeps the old state.
|
|
40271
|
+
*/
|
|
40272
|
+
async applyToolGrantManifest(raw) {
|
|
40273
|
+
const pin = this.cfg.ownerGrantPin?.pin;
|
|
40274
|
+
if (!pin) {
|
|
40275
|
+
this.logManifest("no_pin");
|
|
40276
|
+
return;
|
|
40277
|
+
}
|
|
40278
|
+
if (raw === void 0 || raw === null) {
|
|
40279
|
+
this.logManifest("no_manifest");
|
|
40280
|
+
return;
|
|
40281
|
+
}
|
|
40282
|
+
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
|
|
40283
|
+
if (v.state === "ok") {
|
|
40284
|
+
try {
|
|
40285
|
+
await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
|
|
40286
|
+
} catch {
|
|
40287
|
+
this.logManifest("kept_last_good");
|
|
40288
|
+
return;
|
|
40289
|
+
}
|
|
40290
|
+
this.toolGrantManifest = v.manifest;
|
|
40291
|
+
this.toolGrantCounter = v.manifest.counter;
|
|
40292
|
+
this.logManifest("ok");
|
|
40293
|
+
if (v.droppedUnknown) this.logManifest("unknown_entry");
|
|
40294
|
+
return;
|
|
40295
|
+
}
|
|
40296
|
+
this.logManifest(v.state);
|
|
40297
|
+
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
40298
|
+
}
|
|
40299
|
+
logManifest(state) {
|
|
40300
|
+
this.manifestState = state;
|
|
40301
|
+
const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
|
|
40302
|
+
writeServiceLog2(quiet ? "info" : "warn", "authz.manifest", {
|
|
40303
|
+
state: ServiceLogEnum.of(state, [
|
|
40304
|
+
"ok",
|
|
40305
|
+
"unchanged",
|
|
40306
|
+
"no_pin",
|
|
40307
|
+
"no_manifest",
|
|
40308
|
+
"malformed",
|
|
40309
|
+
"unsupported_algo",
|
|
40310
|
+
"bad_signature",
|
|
40311
|
+
"wrong_agent",
|
|
40312
|
+
"wrong_owner",
|
|
40313
|
+
"rollback",
|
|
40314
|
+
"unknown_entry",
|
|
40315
|
+
"kept_last_good"
|
|
40316
|
+
])
|
|
40317
|
+
});
|
|
40318
|
+
}
|
|
38781
40319
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
38782
|
-
async authorizeTurn(channelId, messageId, senderId, relaySpeakerId) {
|
|
40320
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
38783
40321
|
const held = await this.heldPosition(channelId);
|
|
38784
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId);
|
|
38785
|
-
if (auth.tool_grants
|
|
40322
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
40323
|
+
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
38786
40324
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
38787
40325
|
writeServiceLog2("warn", "room_grant.untracked");
|
|
38788
40326
|
return Object.freeze({ ...auth, tool_grants: NO_TOOL_GRANTS });
|
|
@@ -38864,9 +40402,48 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38864
40402
|
if (this.roomDefaultGrants && this.perToolConsumer !== null && epochFresh && meta?.is_dm === false && audience === "team_room" && entry.crossOrg === false) {
|
|
38865
40403
|
tool_grants = ROOM_DEFAULT_READ_GRANTS;
|
|
38866
40404
|
}
|
|
40405
|
+
let grantSource = tool_grants.length ? "constant" : "none";
|
|
40406
|
+
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
40407
|
+
const binding = this.manifestBinding;
|
|
40408
|
+
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40409
|
+
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40410
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40411
|
+
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
|
|
40412
|
+
const r = effectiveToolGrants({
|
|
40413
|
+
audience: "owner_dm",
|
|
40414
|
+
base: OWNER_DM_DEFAULT_READ_GRANTS,
|
|
40415
|
+
manifest,
|
|
40416
|
+
eligible,
|
|
40417
|
+
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
40418
|
+
disabled: this.payloadToolDeny,
|
|
40419
|
+
signedTargets,
|
|
40420
|
+
roomSearchCapped: false
|
|
40421
|
+
});
|
|
40422
|
+
tool_grants = r.grants;
|
|
40423
|
+
grantSource = r.source;
|
|
40424
|
+
} else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
|
|
40425
|
+
const r = effectiveToolGrants({
|
|
40426
|
+
audience: "team_room",
|
|
40427
|
+
base: ROOM_DEFAULT_READ_GRANTS,
|
|
40428
|
+
manifest,
|
|
40429
|
+
eligible: boundNow && this.manifestConsumer !== null,
|
|
40430
|
+
manifestSwitchOn: this.roomManifestGrants,
|
|
40431
|
+
disabled: this.payloadToolDeny,
|
|
40432
|
+
signedTargets,
|
|
40433
|
+
roomSearchCapped: manifest !== null && this.roomSearchCapped(channelId)
|
|
40434
|
+
});
|
|
40435
|
+
tool_grants = r.grants;
|
|
40436
|
+
grantSource = r.source;
|
|
40437
|
+
}
|
|
40438
|
+
if (opts.ownerVerdict?.kind === "failed") {
|
|
40439
|
+
tool_grants = NO_TOOL_GRANTS;
|
|
40440
|
+
memoryWrite = false;
|
|
40441
|
+
relaySpeaker = void 0;
|
|
40442
|
+
grantSource = "none";
|
|
40443
|
+
}
|
|
38867
40444
|
const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
|
|
38868
40445
|
if (opts.log !== false) {
|
|
38869
|
-
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite);
|
|
40446
|
+
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
|
|
38870
40447
|
}
|
|
38871
40448
|
return Object.freeze({
|
|
38872
40449
|
version: 1,
|
|
@@ -38902,8 +40479,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38902
40479
|
* enforcement is attached
|
|
38903
40480
|
* memory_write the turn carries memory_write (Markdown memory
|
|
38904
40481
|
* 7a, spec §8.3): a boolean, never content
|
|
40482
|
+
* grant_source where the grants came from: constant | none in
|
|
40483
|
+
* step 1 (owner-signed tool grants §7.6)
|
|
38905
40484
|
*/
|
|
38906
|
-
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite) {
|
|
40485
|
+
logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite, grantSource) {
|
|
38907
40486
|
const meta = this.freshMeta(channelId);
|
|
38908
40487
|
const roster = this.freshRoster(channelId);
|
|
38909
40488
|
const sender = roster?.get(senderId);
|
|
@@ -38936,7 +40515,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38936
40515
|
epoch_fresh: epochFresh,
|
|
38937
40516
|
room_kill_switch: ServiceLogEnum.of(this.roomDefaultGrants ? "on" : "off", ["on", "off"]),
|
|
38938
40517
|
per_tool_consumer: this.perToolConsumer !== null,
|
|
38939
|
-
memory_write: memoryWrite
|
|
40518
|
+
memory_write: memoryWrite,
|
|
40519
|
+
grant_source: ServiceLogEnum.of(grantSource, ["constant", "manifest", "fallback", "none"])
|
|
38940
40520
|
});
|
|
38941
40521
|
}
|
|
38942
40522
|
/**
|
|
@@ -39529,6 +41109,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39529
41109
|
this.joinState.set(cid, "JOINED");
|
|
39530
41110
|
}
|
|
39531
41111
|
if (!entries.length) return;
|
|
41112
|
+
await this.maybePruneOwnerReplay();
|
|
39532
41113
|
const receivedAt = Date.now();
|
|
39533
41114
|
const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
|
|
39534
41115
|
const itemCaptureAtReceipt = /* @__PURE__ */ new Map();
|
|
@@ -39621,7 +41202,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39621
41202
|
acked.push(m.queueId);
|
|
39622
41203
|
continue;
|
|
39623
41204
|
}
|
|
39624
|
-
const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
|
|
41205
|
+
const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
|
|
39625
41206
|
await this.prepareNotesForTurn(m.channelId, refAuth);
|
|
39626
41207
|
const vaultRefMessage = {
|
|
39627
41208
|
channel_id: m.channelId,
|
|
@@ -39681,7 +41262,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39681
41262
|
{ attachmentsDir: this.attachmentsDirImpl, channelId: m.channelId, messageId: m.messageId },
|
|
39682
41263
|
this.receiveDeps()
|
|
39683
41264
|
);
|
|
39684
|
-
const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
|
|
41265
|
+
const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
|
|
39685
41266
|
await this.prepareNotesForTurn(m.channelId, shareAuth);
|
|
39686
41267
|
const runtimeMessage = {
|
|
39687
41268
|
channel_id: m.channelId,
|
|
@@ -39749,14 +41330,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39749
41330
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
39750
41331
|
m.envelope.sender_id
|
|
39751
41332
|
)?.name;
|
|
41333
|
+
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
39752
41334
|
const turnAuthorization = await this.authorizeTurn(
|
|
39753
41335
|
m.channelId,
|
|
39754
41336
|
m.messageId,
|
|
39755
41337
|
m.envelope.sender_id,
|
|
41338
|
+
ownerVerdict,
|
|
39756
41339
|
speaker?.identityId
|
|
39757
41340
|
);
|
|
39758
41341
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
39759
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
|
|
41342
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
39760
41343
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
39761
41344
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
39762
41345
|
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
@@ -39787,7 +41370,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39787
41370
|
sender_id: m.envelope.sender_id,
|
|
39788
41371
|
text: turnText,
|
|
39789
41372
|
ts: m.receivedAt,
|
|
39790
|
-
|
|
41373
|
+
// Owner grants plan 4 Task 16 (M-6): under a manifest a call-start greeting entry carries no grants and no
|
|
41374
|
+
// memory_write, on every branch (skip, held, heldAfterAck spreads held). Unpinned agents unchanged (G-4).
|
|
41375
|
+
authorization: fastGreeting && this.toolGrantManifest !== null ? greetingAuthorization(turnAuthorization) : turnAuthorization,
|
|
39791
41376
|
memory,
|
|
39792
41377
|
// A relay-attributed turn is the SPEAKER's message to the runtime:
|
|
39793
41378
|
// name it after them and never flag it as agent-authored.
|
|
@@ -39811,7 +41396,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39811
41396
|
open2.ids.push(m.messageId);
|
|
39812
41397
|
open2.parts.push(turnText);
|
|
39813
41398
|
const staging = (async () => {
|
|
39814
|
-
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory));
|
|
41399
|
+
const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory), { absorbed: true });
|
|
39815
41400
|
if (!ok) {
|
|
39816
41401
|
const i = open2.ids.indexOf(m.messageId);
|
|
39817
41402
|
if (i > 0) {
|
|
@@ -39950,11 +41535,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39950
41535
|
void work.finally(() => this.unifiedWork.delete(work));
|
|
39951
41536
|
}
|
|
39952
41537
|
}
|
|
39953
|
-
} else if (this.recallProviderOn()) {
|
|
41538
|
+
} else if (this.recallProviderOn() && ownerVerdict.kind !== "failed") {
|
|
39954
41539
|
const recallTap = this.captureRecallTap(m.messageId, m.channelId);
|
|
39955
41540
|
this.spawnRecallTap({ message: runtimeEntry(false, { recent: [], facts: [] }), captured: recallTap });
|
|
39956
41541
|
}
|
|
39957
|
-
void recordInboundMemory(
|
|
41542
|
+
if (ownerVerdict.kind !== "failed") void recordInboundMemory(
|
|
39958
41543
|
this.memory,
|
|
39959
41544
|
m.channelId,
|
|
39960
41545
|
m.envelope.sender_id,
|
|
@@ -39977,6 +41562,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39977
41562
|
} catch {
|
|
39978
41563
|
}
|
|
39979
41564
|
}
|
|
41565
|
+
this.ownerReplayPending.clear();
|
|
39980
41566
|
this.latency.record("recv.cycle", performance.now() - cycleStart);
|
|
39981
41567
|
if (this.buffer.length) {
|
|
39982
41568
|
for (const l of this.listeners) l();
|
|
@@ -40015,7 +41601,46 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40015
41601
|
return () => this.listeners.delete(listener);
|
|
40016
41602
|
}
|
|
40017
41603
|
/** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
|
|
41604
|
+
/** Review H-1: every buffer exit narrows manifest grants again to the state at delivery. */
|
|
40018
41605
|
takeBuffered(channelId) {
|
|
41606
|
+
return this.takeBufferedRaw(channelId).map((m) => this.delivering(m));
|
|
41607
|
+
}
|
|
41608
|
+
takeDeliverable(busyChannels) {
|
|
41609
|
+
return this.takeDeliverableRaw(busyChannels).map((m) => this.delivering(m));
|
|
41610
|
+
}
|
|
41611
|
+
/** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
|
|
41612
|
+
delivering(m) {
|
|
41613
|
+
const out = this.renarrowManifestGrants(m);
|
|
41614
|
+
this.noteDeliveredFrame(out);
|
|
41615
|
+
return out;
|
|
41616
|
+
}
|
|
41617
|
+
/**
|
|
41618
|
+
* Review H-1 (kill switches reach staged and restored turns): a grant marked source "manifest" survives delivery
|
|
41619
|
+
* only while its audience's manifest switch is on, the binding is bound at the applied counter, and the current
|
|
41620
|
+
* manifest still signs the tool. Otherwise it becomes the fallback: kept unmarked if it is a constant of the
|
|
41621
|
+
* audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
|
|
41622
|
+
*/
|
|
41623
|
+
renarrowManifestGrants(m) {
|
|
41624
|
+
const a = m.authorization;
|
|
41625
|
+
if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
|
|
41626
|
+
const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
|
|
41627
|
+
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
41628
|
+
const b = this.manifestBinding;
|
|
41629
|
+
const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
|
|
41630
|
+
const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
|
|
41631
|
+
const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
|
|
41632
|
+
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) : []);
|
|
41633
|
+
let changed = false;
|
|
41634
|
+
const out = a.tool_grants.flatMap((g) => {
|
|
41635
|
+
if (!(g.mode === "read" && g.source === "manifest")) return [g];
|
|
41636
|
+
if (switchOn && bound && signedNow.has(g.tool)) return [g];
|
|
41637
|
+
changed = true;
|
|
41638
|
+
return constants4.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
41639
|
+
});
|
|
41640
|
+
if (!changed) return m;
|
|
41641
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
|
|
41642
|
+
}
|
|
41643
|
+
takeBufferedRaw(channelId) {
|
|
40019
41644
|
if (channelId === void 0 && this.pausedChannels.size === 0 && this.supersededChannels.size === 0) {
|
|
40020
41645
|
return this.buffer.splice(0);
|
|
40021
41646
|
}
|
|
@@ -40038,7 +41663,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40038
41663
|
* working on — which it used to refuse with a "busy" reply and ack — and a
|
|
40039
41664
|
* backlog replays one message per channel at a time after a restart.
|
|
40040
41665
|
*/
|
|
40041
|
-
|
|
41666
|
+
takeDeliverableRaw(busyChannels) {
|
|
40042
41667
|
const picked = /* @__PURE__ */ new Set();
|
|
40043
41668
|
const out = [];
|
|
40044
41669
|
const keep = [];
|
|
@@ -40198,6 +41823,107 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40198
41823
|
}
|
|
40199
41824
|
this.noticeDeadLetter(message.channel_id);
|
|
40200
41825
|
}
|
|
41826
|
+
/** Spec §7.5: the room is capped today: cap 0, reached (by the runtime or the backstop), or at the cap. Fails closed. */
|
|
41827
|
+
roomSearchCapped(channelId) {
|
|
41828
|
+
if (this.roomSearchDailyCap === 0 || this.agentRoomSearchDailyCap === 0) return true;
|
|
41829
|
+
try {
|
|
41830
|
+
const day = utcDayOf(Date.now());
|
|
41831
|
+
const st = this.store.roomSearchCapState(channelId, day);
|
|
41832
|
+
if (this.store.roomSearchCapState(AGENT_TOTAL_KEY, day).grantedTurns >= this.agentRoomSearchDailyCap) return true;
|
|
41833
|
+
return st.reachedBy !== null || st.grantedTurns >= this.roomSearchDailyCap;
|
|
41834
|
+
} catch {
|
|
41835
|
+
return true;
|
|
41836
|
+
}
|
|
41837
|
+
}
|
|
41838
|
+
/** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
|
|
41839
|
+
noteRoomCapReached(channelId) {
|
|
41840
|
+
if (typeof channelId !== "string" || this.closingDown) return;
|
|
41841
|
+
if (this.roomSearchDailyCap === 0) return;
|
|
41842
|
+
if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
|
|
41843
|
+
const day = utcDayOf(Date.now());
|
|
41844
|
+
try {
|
|
41845
|
+
if (this.store.markRoomSearchCapReached(channelId, day, "runtime")) this.logRoomCap(day, channelId, "runtime");
|
|
41846
|
+
if (this.store.roomSearchCapState(channelId, day).noticePosted) return;
|
|
41847
|
+
} catch (e) {
|
|
41848
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
41849
|
+
return;
|
|
41850
|
+
}
|
|
41851
|
+
this.noticeRoomSearchCap(channelId, day);
|
|
41852
|
+
}
|
|
41853
|
+
/** K-14, once per room per UTC day; the flag is persisted only after the send resolves. Called only from noteRoomCapReached. */
|
|
41854
|
+
noticeRoomSearchCap(channelId, day) {
|
|
41855
|
+
const key = `${day}
|
|
41856
|
+
${channelId}`;
|
|
41857
|
+
if (this.roomCapNoticePending.has(key)) return;
|
|
41858
|
+
this.roomCapNoticePending.add(key);
|
|
41859
|
+
let pending;
|
|
41860
|
+
try {
|
|
41861
|
+
pending = this.send(channelId, ROOM_SEARCH_CAP_NOTICE, void 0, { agentTurn: false });
|
|
41862
|
+
} catch (e) {
|
|
41863
|
+
pending = Promise.reject(e);
|
|
41864
|
+
}
|
|
41865
|
+
void pending.then(
|
|
41866
|
+
() => {
|
|
41867
|
+
try {
|
|
41868
|
+
this.store.markRoomSearchCapNoticePosted(channelId, day);
|
|
41869
|
+
} catch (e) {
|
|
41870
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
|
|
41871
|
+
}
|
|
41872
|
+
writeServiceLog2("info", "authz.room_cap", { state: ServiceLogEnum.of("notice_sent", ["notice_sent"]) });
|
|
41873
|
+
},
|
|
41874
|
+
(e) => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("notice_failed", ["notice_failed"]) }, e)
|
|
41875
|
+
).finally(() => this.roomCapNoticePending.delete(key));
|
|
41876
|
+
}
|
|
41877
|
+
logRoomCap(day, channelId, by) {
|
|
41878
|
+
const key = `${day}
|
|
41879
|
+
${channelId}`;
|
|
41880
|
+
if (this.roomCapLogged.has(key)) return;
|
|
41881
|
+
if (this.roomCapLogged.size > 5e3) this.roomCapLogged.clear();
|
|
41882
|
+
this.roomCapLogged.add(key);
|
|
41883
|
+
writeServiceLog2("info", "authz.room_cap", {
|
|
41884
|
+
state: ServiceLogEnum.of("reached", ["reached"]),
|
|
41885
|
+
by: ServiceLogEnum.of(by, ["runtime", "bridge", "operator"])
|
|
41886
|
+
});
|
|
41887
|
+
}
|
|
41888
|
+
/**
|
|
41889
|
+
* Spec §7.5 (T-14b, audit r1 H-3): the silent bridge backstop. Counts a team_room turn that actually goes out
|
|
41890
|
+
* to the runtime with web_search, once per message id (callers run it after their runtimePendingIds early
|
|
41891
|
+
* return). Past the cap, and on a store failure (fail closed), the entry loses web_search. Never posts K-14.
|
|
41892
|
+
*/
|
|
41893
|
+
applyRoomSearchBackstop(entry) {
|
|
41894
|
+
const a = entry.authorization;
|
|
41895
|
+
if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
|
|
41896
|
+
const day = utcDayOf(Date.now());
|
|
41897
|
+
if (this.roomSearchDailyCap === 0) {
|
|
41898
|
+
this.logRoomCap(day, entry.channel_id, "operator");
|
|
41899
|
+
return this.withoutWebSearch(entry);
|
|
41900
|
+
}
|
|
41901
|
+
if (this.agentRoomSearchDailyCap === 0) {
|
|
41902
|
+
this.logRoomCap(day, AGENT_TOTAL_KEY, "operator");
|
|
41903
|
+
return this.withoutWebSearch(entry);
|
|
41904
|
+
}
|
|
41905
|
+
try {
|
|
41906
|
+
const n = this.store.countRoomSearchGrant(entry.channel_id, day);
|
|
41907
|
+
if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
|
|
41908
|
+
this.logRoomCap(day, entry.channel_id, "bridge");
|
|
41909
|
+
}
|
|
41910
|
+
if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
|
|
41911
|
+
const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
|
|
41912
|
+
if (total === this.agentRoomSearchDailyCap) this.logRoomCap(day, AGENT_TOTAL_KEY, "bridge");
|
|
41913
|
+
if (total > this.agentRoomSearchDailyCap) return this.withoutWebSearch(entry);
|
|
41914
|
+
return entry;
|
|
41915
|
+
} catch (e) {
|
|
41916
|
+
writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("count_failed", ["count_failed"]) }, e);
|
|
41917
|
+
return this.withoutWebSearch(entry);
|
|
41918
|
+
}
|
|
41919
|
+
}
|
|
41920
|
+
withoutWebSearch(entry) {
|
|
41921
|
+
const a = entry.authorization;
|
|
41922
|
+
return {
|
|
41923
|
+
...entry,
|
|
41924
|
+
authorization: Object.freeze({ ...a, tool_grants: Object.freeze(a.tool_grants.filter((g) => g.tool !== "web_search")) })
|
|
41925
|
+
};
|
|
41926
|
+
}
|
|
40201
41927
|
noticeDeadLetter(channelId) {
|
|
40202
41928
|
if (this.closingDown) return;
|
|
40203
41929
|
const now = Date.now();
|
|
@@ -40419,13 +42145,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40419
42145
|
modeEpoch: this.store.recallState().modeEpoch
|
|
40420
42146
|
};
|
|
40421
42147
|
let persisted = { stamp, recallPurgeEpoch: recall.purgeEpoch, recallModeEpoch: recall.modeEpoch };
|
|
40422
|
-
|
|
40423
|
-
|
|
40424
|
-
|
|
40425
|
-
|
|
40426
|
-
|
|
40427
|
-
|
|
40428
|
-
|
|
42148
|
+
const replay = this.ownerReplayPending.get(message.message_id);
|
|
42149
|
+
try {
|
|
42150
|
+
await this.store.stageRuntimeInbox(message.message_id, this.stagedPayload(message, stamp, recall), message.ts, (prior, next) => {
|
|
42151
|
+
const a = splitIngestStamp(prior);
|
|
42152
|
+
const b = splitIngestStamp(next);
|
|
42153
|
+
if (a === null || b === null || a.message !== b.message) return false;
|
|
42154
|
+
persisted = a.marks;
|
|
42155
|
+
return true;
|
|
42156
|
+
}, replay ? { ...replay, processedAt: Date.now() } : void 0);
|
|
42157
|
+
} finally {
|
|
42158
|
+
this.ownerReplayPending.delete(message.message_id);
|
|
42159
|
+
}
|
|
40429
42160
|
this.applyStagedMarks(message.message_id, persisted);
|
|
40430
42161
|
}
|
|
40431
42162
|
/** In-memory mirror of the persisted marks (staging and restore). */
|
|
@@ -40445,6 +42176,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40445
42176
|
const message = rest;
|
|
40446
42177
|
this.applyStagedMarks(message.message_id, marks);
|
|
40447
42178
|
this.runtimePendingIds.add(message.message_id);
|
|
42179
|
+
if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
|
|
42180
|
+
this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
|
|
42181
|
+
}
|
|
40448
42182
|
if (this.unifiedOn) this.deferredUnifiedIds.add(message.message_id);
|
|
40449
42183
|
else this.deferredRecallIds.add(message.message_id);
|
|
40450
42184
|
if (row.deliveryAttempts > 0) {
|
|
@@ -40461,12 +42195,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40461
42195
|
* Returning false is backpressure: the caller leaves the backend row unacked
|
|
40462
42196
|
* so it remains the durable queue rather than shedding accepted work.
|
|
40463
42197
|
*/
|
|
40464
|
-
async stageRuntimeMessage(
|
|
40465
|
-
if (this.runtimePendingIds.has(
|
|
42198
|
+
async stageRuntimeMessage(staging, opts = {}) {
|
|
42199
|
+
if (this.runtimePendingIds.has(staging.message_id)) return true;
|
|
40466
42200
|
if (this.runtimePendingIds.size >= this.maxBuffer) {
|
|
40467
42201
|
writeServiceLog2("warn", "runtime_inbox.full");
|
|
40468
42202
|
return false;
|
|
40469
42203
|
}
|
|
42204
|
+
const message = opts.absorbed ? staging : this.applyRoomSearchBackstop(staging);
|
|
40470
42205
|
try {
|
|
40471
42206
|
await this.stageSealed(message);
|
|
40472
42207
|
this.runtimePendingIds.add(message.message_id);
|
|
@@ -40572,6 +42307,37 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40572
42307
|
perToolConsumerAttached() {
|
|
40573
42308
|
return this.perToolConsumer !== null;
|
|
40574
42309
|
}
|
|
42310
|
+
/**
|
|
42311
|
+
* Owner grants (spec §7.9, plan 4 Task 5): the attached per-tool runtime socket whose hello
|
|
42312
|
+
* declared manifest_grants_v1, or null. A manifest grant is issued only while one is attached
|
|
42313
|
+
* (Task 13); every other socket receives manifest grants narrowed to the constants.
|
|
42314
|
+
* Newest wins, like perToolConsumer.
|
|
42315
|
+
*/
|
|
42316
|
+
manifestConsumer = null;
|
|
42317
|
+
/** OQ-5: the latest hello's class; "missing" until any hello. Reported in tool_grant_status (Task 15). */
|
|
42318
|
+
lastConsumerCapability = "missing";
|
|
42319
|
+
noteManifestConsumer(token) {
|
|
42320
|
+
this.manifestConsumer = token;
|
|
42321
|
+
}
|
|
42322
|
+
clearManifestConsumer(token) {
|
|
42323
|
+
if (this.manifestConsumer === token) this.manifestConsumer = null;
|
|
42324
|
+
}
|
|
42325
|
+
manifestConsumerAttached() {
|
|
42326
|
+
return this.manifestConsumer !== null;
|
|
42327
|
+
}
|
|
42328
|
+
noteConsumerCapability(state) {
|
|
42329
|
+
this.lastConsumerCapability = state;
|
|
42330
|
+
if (state !== "ok") {
|
|
42331
|
+
writeServiceLog2("info", "authz.capability", { state: ServiceLogEnum.of(state, ["missing", "harness_consumer"]) });
|
|
42332
|
+
}
|
|
42333
|
+
}
|
|
42334
|
+
/**
|
|
42335
|
+
* Harness re-rule (2026-10-12, audit r2 H-3): true exactly when this agent has a pin loaded and a good
|
|
42336
|
+
* manifest. A harness consumer then receives [] in every audience (consumer-grants.ts withHarnessGrants).
|
|
42337
|
+
*/
|
|
42338
|
+
harnessGrantsWithheld() {
|
|
42339
|
+
return this.cfg.ownerGrantPin?.pin != null && this.toolGrantManifest !== null;
|
|
42340
|
+
}
|
|
40575
42341
|
health() {
|
|
40576
42342
|
let joined = 0;
|
|
40577
42343
|
for (const s of this.joinState.values()) if (s === "JOINED") joined++;
|
|
@@ -40802,11 +42568,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40802
42568
|
};
|
|
40803
42569
|
|
|
40804
42570
|
// src/config.ts
|
|
40805
|
-
import { readFileSync as
|
|
40806
|
-
import { dirname as
|
|
42571
|
+
import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
|
|
42572
|
+
import { dirname as dirname6, join as join18 } from "node:path";
|
|
40807
42573
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
40808
42574
|
function loadConfig(opts) {
|
|
40809
|
-
const raw =
|
|
42575
|
+
const raw = readFileSync10(opts.credentialPath, "utf8");
|
|
40810
42576
|
try {
|
|
40811
42577
|
const mode = statSync5(opts.credentialPath).mode & 511;
|
|
40812
42578
|
if (mode & 63) writeServiceLog2("warn", "config.credential_permissions_unsafe");
|
|
@@ -40819,7 +42585,11 @@ function loadConfig(opts) {
|
|
|
40819
42585
|
throw new Error("credential file is not valid JSON");
|
|
40820
42586
|
}
|
|
40821
42587
|
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 ??
|
|
42588
|
+
const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
|
|
42589
|
+
const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
|
|
42590
|
+
writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
|
|
42591
|
+
state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
|
|
42592
|
+
});
|
|
40823
42593
|
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
42594
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
40825
42595
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
@@ -40877,7 +42647,8 @@ function loadConfig(opts) {
|
|
|
40877
42647
|
voice,
|
|
40878
42648
|
voiceFastPathMode: parseFastPathMode(json2.voice_fast_path_mode),
|
|
40879
42649
|
modelSource,
|
|
40880
|
-
memoryEnabled
|
|
42650
|
+
memoryEnabled,
|
|
42651
|
+
ownerGrantPin
|
|
40881
42652
|
};
|
|
40882
42653
|
}
|
|
40883
42654
|
function arg(argv, flag) {
|
|
@@ -40921,11 +42692,11 @@ import { z as z3 } from "zod";
|
|
|
40921
42692
|
|
|
40922
42693
|
// src/auth.ts
|
|
40923
42694
|
import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
40924
|
-
import { existsSync as
|
|
42695
|
+
import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
|
|
40925
42696
|
function resolveToken(opts) {
|
|
40926
42697
|
if (opts.token) return opts.token;
|
|
40927
42698
|
const f = opts.tokenFile;
|
|
40928
|
-
if (f &&
|
|
42699
|
+
if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
|
|
40929
42700
|
const tok = randomBytes11(32).toString("base64url");
|
|
40930
42701
|
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
40931
42702
|
return tok;
|
|
@@ -41017,6 +42788,29 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
41017
42788
|
const { memory_write: _dropped, ...rest } = a;
|
|
41018
42789
|
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
41019
42790
|
}
|
|
42791
|
+
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
42792
|
+
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
42793
|
+
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
42794
|
+
team_room: Object.freeze(["current_time"])
|
|
42795
|
+
});
|
|
42796
|
+
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
42797
|
+
function withManifestCapability(m, manifestConsumer) {
|
|
42798
|
+
if (manifestConsumer) return m;
|
|
42799
|
+
const a = m.authorization;
|
|
42800
|
+
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
42801
|
+
const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
42802
|
+
const narrowed = a.tool_grants.flatMap((g) => {
|
|
42803
|
+
if (!isManifestGrant(g)) return [g];
|
|
42804
|
+
return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
42805
|
+
});
|
|
42806
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
42807
|
+
}
|
|
42808
|
+
function withHarnessGrants(m, withheld) {
|
|
42809
|
+
if (!withheld) return m;
|
|
42810
|
+
const a = m.authorization;
|
|
42811
|
+
if (!a || a.tool_grants.length === 0) return m;
|
|
42812
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
42813
|
+
}
|
|
41020
42814
|
|
|
41021
42815
|
// src/http-server.ts
|
|
41022
42816
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -41038,7 +42832,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
41038
42832
|
return [];
|
|
41039
42833
|
}
|
|
41040
42834
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
41041
|
-
const
|
|
42835
|
+
const withheld = agent.harnessGrantsWithheld();
|
|
42836
|
+
const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
|
|
41042
42837
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
41043
42838
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
41044
42839
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -41305,7 +43100,7 @@ function createHttpServer(agent, opts) {
|
|
|
41305
43100
|
if (method === "GET" && path === "/v1/messages") {
|
|
41306
43101
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
41307
43102
|
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)));
|
|
43103
|
+
return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
|
|
41309
43104
|
}
|
|
41310
43105
|
if (method === "GET" && path === "/v1/events") {
|
|
41311
43106
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -41472,6 +43267,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41472
43267
|
const session = {
|
|
41473
43268
|
consumer: "unknown",
|
|
41474
43269
|
perToolGrants: false,
|
|
43270
|
+
manifestGrants: false,
|
|
41475
43271
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
41476
43272
|
deliveryOpen: helloWaitMs <= 0,
|
|
41477
43273
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -41524,7 +43320,15 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41524
43320
|
retired = true;
|
|
41525
43321
|
continue;
|
|
41526
43322
|
}
|
|
41527
|
-
const out = withConsumerNotes(
|
|
43323
|
+
const out = withConsumerNotes(
|
|
43324
|
+
withManifestCapability(
|
|
43325
|
+
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
43326
|
+
session.manifestGrants
|
|
43327
|
+
),
|
|
43328
|
+
batch[i],
|
|
43329
|
+
session,
|
|
43330
|
+
agent
|
|
43331
|
+
);
|
|
41528
43332
|
const recallOut = withConsumerRecall(out.message, batch[i], session, agent);
|
|
41529
43333
|
const unifiedOut = withConsumerUnified(recallOut.message, batch[i], session, agent);
|
|
41530
43334
|
const frame = { type: "message", ...unifiedOut.message };
|
|
@@ -41573,6 +43377,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
41573
43377
|
if (current?.sock === sock) current = null;
|
|
41574
43378
|
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
41575
43379
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
43380
|
+
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
41576
43381
|
};
|
|
41577
43382
|
current = { sock, cleanup, sweepStale };
|
|
41578
43383
|
sock.on("close", cleanup);
|
|
@@ -41652,6 +43457,15 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41652
43457
|
session.perToolGrants = false;
|
|
41653
43458
|
agent.clearPerToolConsumer(session);
|
|
41654
43459
|
}
|
|
43460
|
+
const manifest = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE);
|
|
43461
|
+
if (manifest && !session.manifestGrants) {
|
|
43462
|
+
session.manifestGrants = true;
|
|
43463
|
+
agent.noteManifestConsumer(session);
|
|
43464
|
+
} else if (!manifest && session.manifestGrants) {
|
|
43465
|
+
session.manifestGrants = false;
|
|
43466
|
+
agent.clearManifestConsumer(session);
|
|
43467
|
+
}
|
|
43468
|
+
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
41655
43469
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
41656
43470
|
agent.noteNotesConsumer();
|
|
41657
43471
|
}
|
|
@@ -41659,7 +43473,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41659
43473
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
41660
43474
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
41661
43475
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
41662
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : []] });
|
|
43476
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
|
|
41663
43477
|
if (!session.deliveryOpen) {
|
|
41664
43478
|
session.deliveryOpen = true;
|
|
41665
43479
|
flush();
|
|
@@ -41872,8 +43686,17 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
41872
43686
|
}
|
|
41873
43687
|
return;
|
|
41874
43688
|
}
|
|
43689
|
+
if (m.type === "room_cap_reached") {
|
|
43690
|
+
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
43691
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
43692
|
+
return;
|
|
43693
|
+
}
|
|
43694
|
+
agent.noteRoomCapReached(m.channel_id);
|
|
43695
|
+
return;
|
|
43696
|
+
}
|
|
41875
43697
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
41876
43698
|
}
|
|
43699
|
+
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
41877
43700
|
export {
|
|
41878
43701
|
BRIDGE_ERRORS,
|
|
41879
43702
|
BRIDGE_VERSION,
|