@securecoms/agent-bridge 0.26.3 → 0.26.5
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 +11 -0
- package/connectors/hermes/adapter.py +202 -3
- package/connectors/hermes/securecoms_authz.py +190 -0
- package/connectors/hermes/tests/test_adapter_wiring.py +481 -3
- package/connectors/hermes/tests/test_securecoms_authz.py +235 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +19 -4
- package/connectors/openclaw/openclaw-entry.js +184 -3
- package/connectors/openclaw/openclaw-entry.status.test.ts +11 -1
- package/connectors/openclaw/openclaw-entry.toolgate.test.ts +466 -0
- package/dist/bin.js +113 -10
- package/dist/bridge-agent.d.ts +42 -4
- package/dist/harness-report.d.ts +14 -0
- package/dist/index.js +113 -10
- package/dist/service/runtime-config-dir.d.ts +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-prompt.d.ts +9 -0
- package/dist/ws-server.d.ts +0 -77
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -28512,11 +28512,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
|
|
|
28512
28512
|
const marker = "\u2026";
|
|
28513
28513
|
return content.slice(0, Math.max(0, max - marker.length)) + marker;
|
|
28514
28514
|
}
|
|
28515
|
+
var FAST_DEFER_RULE = `Whenever you cannot answer completely from what is shown here, never say you can't, that you lack a tool, access or real-time data: reply with one short sentence like "Let me check that." and end with the exact token [[DEFER]] so the full agent answers next.`;
|
|
28516
|
+
var REFUSAL_PATTERNS = [
|
|
28517
|
+
// "I can't / cannot / am unable to / am not able to <check|access|...>", optional adverb.
|
|
28518
|
+
/\b(?:can't|cannot|can\s+not|unable\s+to|not\s+able\s+to)\s+(?:\w+ly\s+)?(?:check|access|look|see|browse|search|get|provide|give\s+you|find|retrieve|fetch|pull|know|help\s+with\s+(?:that|this)|do\s+that)\b/,
|
|
28519
|
+
// "I don't / do not / doesn't have (a|any|the)? <tool|access|ability|real-time|live ...>".
|
|
28520
|
+
/\b(?:don't|do\s+not|doesn't|does\s+not|lack)\s+(?:have\s+)?(?:a\s+|any\s+|the\s+)?(?:tool|tools|access|ability|capability|real[-\s]?time|live|current|internet|web|way\s+to)\b/,
|
|
28521
|
+
/\bno\s+(?:access|tools?|internet|web\s+access|real[-\s]?time|live\s+data|way\s+to\s+(?:check|access|know|see))\b/,
|
|
28522
|
+
/\bas\s+an\s+(?:ai|a\.i\.|language\s+model)\b/,
|
|
28523
|
+
/\bbeyond\s+my\s+(?:capabilities|abilities|capability)\b/,
|
|
28524
|
+
/\b(?:knowledge\s+cutoff|training\s+data)\b/
|
|
28525
|
+
];
|
|
28526
|
+
function isRefusal(reply) {
|
|
28527
|
+
const t = reply.toLowerCase().replace(/[‘’]/g, "'");
|
|
28528
|
+
return REFUSAL_PATTERNS.some((re) => re.test(t));
|
|
28529
|
+
}
|
|
28515
28530
|
function buildFastMessages(args2) {
|
|
28516
28531
|
const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
|
|
28517
28532
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
28518
28533
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
28519
|
-
const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
|
|
28534
|
+
const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
|
|
28520
28535
|
const recent = args2.memory.recent.map((t) => ({
|
|
28521
28536
|
role: t.role === "agent" ? "assistant" : "user",
|
|
28522
28537
|
content: t.text
|
|
@@ -28775,7 +28790,7 @@ function assertManagedName(name) {
|
|
|
28775
28790
|
}
|
|
28776
28791
|
function isLocalOnlyName(name) {
|
|
28777
28792
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
28778
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json";
|
|
28793
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets";
|
|
28779
28794
|
}
|
|
28780
28795
|
function atomicWrite(path, body, deps) {
|
|
28781
28796
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -28834,6 +28849,26 @@ function lineHash(line) {
|
|
|
28834
28849
|
return createHash2("sha256").update(line.trim(), "utf8").digest("hex");
|
|
28835
28850
|
}
|
|
28836
28851
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
28852
|
+
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
28853
|
+
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
|
|
28854
|
+
function parseSecretPathsReport(raw) {
|
|
28855
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
28856
|
+
const r = raw;
|
|
28857
|
+
const f = r.outside_by_field;
|
|
28858
|
+
if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite)) return void 0;
|
|
28859
|
+
return {
|
|
28860
|
+
mode: r.mode,
|
|
28861
|
+
inside: r.inside,
|
|
28862
|
+
allowlisted: r.allowlisted,
|
|
28863
|
+
outside: r.outside,
|
|
28864
|
+
outside_by_field: {
|
|
28865
|
+
headers_file: f.headers_file,
|
|
28866
|
+
postgres_url_file: f.postgres_url_file,
|
|
28867
|
+
search_key_file: f.search_key_file,
|
|
28868
|
+
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {}
|
|
28869
|
+
}
|
|
28870
|
+
};
|
|
28871
|
+
}
|
|
28837
28872
|
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
28838
28873
|
var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
28839
28874
|
var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
@@ -28902,7 +28937,8 @@ function readLocalReport(home, identityId) {
|
|
|
28902
28937
|
persona,
|
|
28903
28938
|
has_search_key: hasSearchKey,
|
|
28904
28939
|
has_llm_key: hasLlmKey,
|
|
28905
|
-
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source)
|
|
28940
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
28941
|
+
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
28906
28942
|
};
|
|
28907
28943
|
}
|
|
28908
28944
|
function deleteLegacyMemorySources(home, identityId, ids) {
|
|
@@ -29704,6 +29740,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29704
29740
|
fastPathDeferred = 0;
|
|
29705
29741
|
/** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
|
|
29706
29742
|
fastPathIntentDeferred = 0;
|
|
29743
|
+
/** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
|
|
29744
|
+
fastPathRefusalDeferred = 0;
|
|
29707
29745
|
/**
|
|
29708
29746
|
* Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
|
|
29709
29747
|
* verb can tell "bridge process up" (the app's green dot, which only
|
|
@@ -30098,7 +30136,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
30098
30136
|
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
30099
30137
|
// regular-file bytes and strips every local path before this leaves the
|
|
30100
30138
|
// machine.
|
|
30101
|
-
legacy_memory: local?.legacy_memory ?? []
|
|
30139
|
+
legacy_memory: local?.legacy_memory ?? [],
|
|
30140
|
+
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
30141
|
+
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
30142
|
+
// one enum only; readLocalReport rebuilds it so no path rides along.
|
|
30143
|
+
secret_paths: local?.secret_paths ?? null
|
|
30102
30144
|
};
|
|
30103
30145
|
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
30104
30146
|
method: "POST",
|
|
@@ -31981,11 +32023,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31981
32023
|
});
|
|
31982
32024
|
const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
|
|
31983
32025
|
const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
|
|
31984
|
-
const
|
|
31985
|
-
if (!reply) return "failed";
|
|
32026
|
+
const split3 = splitFastReply(raw, 200, maxSentences);
|
|
32027
|
+
if (!split3.reply) return "failed";
|
|
32028
|
+
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
32029
|
+
const defer = split3.defer || refusal;
|
|
32030
|
+
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
31986
32031
|
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
31987
32032
|
this.fastPathSent++;
|
|
31988
32033
|
if (defer) this.fastPathDeferred++;
|
|
32034
|
+
if (refusal) this.fastPathRefusalDeferred++;
|
|
31989
32035
|
return defer ? "deferred" : "answered";
|
|
31990
32036
|
} catch (e) {
|
|
31991
32037
|
this.fastPathFailed++;
|
|
@@ -32322,14 +32368,25 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32322
32368
|
*
|
|
32323
32369
|
* Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
|
|
32324
32370
|
* the kill switch is on, the channel is a DM, and EITHER
|
|
32325
|
-
* (a) audience is owner_dm AND the sender is the pinned
|
|
32326
|
-
* (`channelOwner`, never "anomalous")
|
|
32327
|
-
*
|
|
32371
|
+
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
32372
|
+
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
32373
|
+
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
32374
|
+
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
32375
|
+
* LIMIT below); OR
|
|
32328
32376
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
32329
32377
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
32330
32378
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
32331
32379
|
* owner. A speaker claim from any non-relay sender gets nothing.
|
|
32332
32380
|
* Everything else gets the frozen empty list.
|
|
32381
|
+
*
|
|
32382
|
+
* LIMIT (harness gate review H2, 2026-10-05): "pinned structural owner"
|
|
32383
|
+
* is PER CHANNEL. resolveAndPinOwner pins the single human counterpart of
|
|
32384
|
+
* ANY DM, so in a DM with member Bob, Bob is that channel's pin. What
|
|
32385
|
+
* separates Bob from the real owner is only the server's `is_agent_owner`
|
|
32386
|
+
* flag (connect_channels.py: counterpart user == agent's creator). A
|
|
32387
|
+
* backend that sets that flag wrongly gives Bob the grants, and a harness
|
|
32388
|
+
* connector turns grants into its full toolset. Binding the owner identity
|
|
32389
|
+
* once, outside server control, is an open design follow-up.
|
|
32333
32390
|
*/
|
|
32334
32391
|
authorizationFor(channelId, senderId, relaySpeakerId) {
|
|
32335
32392
|
const meta = this.freshMeta(channelId);
|
|
@@ -33875,6 +33932,31 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33875
33932
|
noteSessionEvicted() {
|
|
33876
33933
|
this.evictions++;
|
|
33877
33934
|
}
|
|
33935
|
+
/** Harness tool gate H4: latest connector self-report (see harness-report.ts). */
|
|
33936
|
+
harness = null;
|
|
33937
|
+
/**
|
|
33938
|
+
* Record a connector's harness report (from its /v1/ws hello). WARN once
|
|
33939
|
+
* each time the attached harness turns to an inactive veto: such a harness
|
|
33940
|
+
* refuses every turn without a tool grant, so its rooms go quiet.
|
|
33941
|
+
*/
|
|
33942
|
+
recordHarnessReport(report) {
|
|
33943
|
+
const wasInactive = this.harness?.tool_veto === "inactive";
|
|
33944
|
+
this.harness = report;
|
|
33945
|
+
if (report.tool_veto === "inactive" && !wasInactive) {
|
|
33946
|
+
writeServiceLog("warn", "harness.tool_veto_inactive", {
|
|
33947
|
+
harness: ServiceLogEnum.of(report.name, ["hermes", "openclaw", "other"])
|
|
33948
|
+
});
|
|
33949
|
+
}
|
|
33950
|
+
}
|
|
33951
|
+
/** Forget `report` when the connector that sent it disconnects. A report
|
|
33952
|
+
* from a newer connector (a different object) is kept. */
|
|
33953
|
+
clearHarnessReport(report) {
|
|
33954
|
+
if (this.harness === report) this.harness = null;
|
|
33955
|
+
}
|
|
33956
|
+
/** The latest harness report, or null (GET /v1/status). */
|
|
33957
|
+
harnessReport() {
|
|
33958
|
+
return this.harness;
|
|
33959
|
+
}
|
|
33878
33960
|
health() {
|
|
33879
33961
|
let joined = 0;
|
|
33880
33962
|
for (const s of this.joinState.values()) if (s === "JOINED") joined++;
|
|
@@ -33937,12 +34019,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33937
34019
|
fast_path_skipped: this.fastPathSkipped,
|
|
33938
34020
|
fast_path_deferred: this.fastPathDeferred,
|
|
33939
34021
|
fast_path_intent_deferred: this.fastPathIntentDeferred,
|
|
34022
|
+
fast_path_refusal_deferred: this.fastPathRefusalDeferred,
|
|
33940
34023
|
fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
|
|
33941
34024
|
voice_settings_version: this.voiceSettingsVersion,
|
|
33942
34025
|
voice_settings_source: this.voiceSettingsSource,
|
|
33943
34026
|
delivery_pings: this.pingsReceived,
|
|
33944
34027
|
agent_sessions: this.authSessions,
|
|
33945
34028
|
evictions: this.evictions,
|
|
34029
|
+
harness_tool_veto_active: this.harness === null ? null : this.harness.tool_veto === "active",
|
|
33946
34030
|
uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
|
|
33947
34031
|
version: BRIDGE_VERSION,
|
|
33948
34032
|
...this.memory.stats?.() ?? {}
|
|
@@ -34367,6 +34451,9 @@ function createHttpServer(agent, opts) {
|
|
|
34367
34451
|
const r = await agent.vaultDelete(parsed.data.enclave_id, parsed.data.vault_channel_id, parsed.data.node_id);
|
|
34368
34452
|
return json(res, 200, r);
|
|
34369
34453
|
}
|
|
34454
|
+
if (method === "GET" && path === "/v1/status") {
|
|
34455
|
+
return json(res, 200, { harness: agent.harnessReport() });
|
|
34456
|
+
}
|
|
34370
34457
|
if (method === "POST" && path === "/v1/status") {
|
|
34371
34458
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
34372
34459
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
@@ -34456,6 +34543,17 @@ function createHttpServer(agent, opts) {
|
|
|
34456
34543
|
return { server, closeAllSseStreams: () => closeSse() };
|
|
34457
34544
|
}
|
|
34458
34545
|
|
|
34546
|
+
// src/harness-report.ts
|
|
34547
|
+
var VERSION_TOKEN = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,39}$/;
|
|
34548
|
+
function parseHarnessReport(raw) {
|
|
34549
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
34550
|
+
const r = raw;
|
|
34551
|
+
const name = r.name === "hermes" || r.name === "openclaw" ? r.name : "other";
|
|
34552
|
+
const version = typeof r.version === "string" && VERSION_TOKEN.test(r.version) ? r.version : null;
|
|
34553
|
+
const tool_veto = r.tool_veto === "active" || r.tool_veto === "inactive" ? r.tool_veto : "unknown";
|
|
34554
|
+
return Object.freeze({ name, version, tool_veto });
|
|
34555
|
+
}
|
|
34556
|
+
|
|
34459
34557
|
// src/ws-server.ts
|
|
34460
34558
|
import { WebSocketServer, WebSocket } from "ws";
|
|
34461
34559
|
var PING_INTERVAL_MS = 3e4;
|
|
@@ -34586,6 +34684,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
34586
34684
|
}
|
|
34587
34685
|
delivered.clear();
|
|
34588
34686
|
if (current?.sock === sock) current = null;
|
|
34687
|
+
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
34589
34688
|
};
|
|
34590
34689
|
current = { sock, cleanup, sweepStale };
|
|
34591
34690
|
sock.on("close", cleanup);
|
|
@@ -34648,7 +34747,11 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
34648
34747
|
return;
|
|
34649
34748
|
}
|
|
34650
34749
|
if (m.type === "hello") {
|
|
34651
|
-
|
|
34750
|
+
const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
|
|
34751
|
+
if (report) {
|
|
34752
|
+
session.harness = report;
|
|
34753
|
+
agent.recordHarnessReport(report);
|
|
34754
|
+
} else session.consumer = "runtime";
|
|
34652
34755
|
sendJson(sock, { type: "hello", features: ["nack"] });
|
|
34653
34756
|
return;
|
|
34654
34757
|
}
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MlsGroupKeyProvider } from "@agentvault/connect-sdk-ts";
|
|
2
|
+
import type { HarnessReport } from "./harness-report.js";
|
|
2
3
|
import type { MemoryContextResult } from "@agentvault/connect-mls-store";
|
|
3
4
|
/**
|
|
4
5
|
* M6 (ADR-0013 Decision H): what the sweep observed about one channel.
|
|
@@ -58,7 +59,10 @@ export type BridgeToolGrant = {
|
|
|
58
59
|
* Tool grants (architect ruling, 2026-10-02): until channel-grant persistence
|
|
59
60
|
* lands, the ONLY non-empty grant list is OWNER_DM_DEFAULT_READ_GRANTS
|
|
60
61
|
* (`current_time` + `web_search`, read mode), and only on a DM whose sender is
|
|
61
|
-
*
|
|
62
|
+
* that DM's pinned human counterpart AND server-marked `is_agent_owner` (or a
|
|
63
|
+
* relayed voice turn whose speaker is). The pin is per channel, so the
|
|
64
|
+
* owner distinction rests on the server's flag: see authorizationFor's LIMIT
|
|
65
|
+
* note. Every
|
|
62
66
|
* other audience, an unpinned or anomalous owner, and the kill switch
|
|
63
67
|
* `SECURECOMS_OWNER_DM_DEFAULT_GRANTS` (anything but unset/`on`) get `[]`.
|
|
64
68
|
* SEC-06 is LIVE for these two tools: docs/security/sec-06-runtime-bounds-audit.md.
|
|
@@ -800,6 +804,8 @@ export declare class BridgeAgent {
|
|
|
800
804
|
private fastPathDeferred;
|
|
801
805
|
/** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
|
|
802
806
|
private fastPathIntentDeferred;
|
|
807
|
+
/** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
|
|
808
|
+
private fastPathRefusalDeferred;
|
|
803
809
|
/**
|
|
804
810
|
* Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
|
|
805
811
|
* verb can tell "bridge process up" (the app's green dot, which only
|
|
@@ -1957,14 +1963,25 @@ export declare class BridgeAgent {
|
|
|
1957
1963
|
*
|
|
1958
1964
|
* Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
|
|
1959
1965
|
* the kill switch is on, the channel is a DM, and EITHER
|
|
1960
|
-
* (a) audience is owner_dm AND the sender is the pinned
|
|
1961
|
-
* (`channelOwner`, never "anomalous")
|
|
1962
|
-
*
|
|
1966
|
+
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
1967
|
+
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
1968
|
+
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
1969
|
+
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
1970
|
+
* LIMIT below); OR
|
|
1963
1971
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
1964
1972
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
1965
1973
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
1966
1974
|
* owner. A speaker claim from any non-relay sender gets nothing.
|
|
1967
1975
|
* Everything else gets the frozen empty list.
|
|
1976
|
+
*
|
|
1977
|
+
* LIMIT (harness gate review H2, 2026-10-05): "pinned structural owner"
|
|
1978
|
+
* is PER CHANNEL. resolveAndPinOwner pins the single human counterpart of
|
|
1979
|
+
* ANY DM, so in a DM with member Bob, Bob is that channel's pin. What
|
|
1980
|
+
* separates Bob from the real owner is only the server's `is_agent_owner`
|
|
1981
|
+
* flag (connect_channels.py: counterpart user == agent's creator). A
|
|
1982
|
+
* backend that sets that flag wrongly gives Bob the grants, and a harness
|
|
1983
|
+
* connector turns grants into its full toolset. Binding the owner identity
|
|
1984
|
+
* once, outside server control, is an open design follow-up.
|
|
1968
1985
|
*/
|
|
1969
1986
|
private authorizationFor;
|
|
1970
1987
|
/**
|
|
@@ -2296,6 +2313,19 @@ export declare class BridgeAgent {
|
|
|
2296
2313
|
* ws-server.ts right after prior.cleanup() in the eviction path.
|
|
2297
2314
|
*/
|
|
2298
2315
|
noteSessionEvicted(): void;
|
|
2316
|
+
/** Harness tool gate H4: latest connector self-report (see harness-report.ts). */
|
|
2317
|
+
private harness;
|
|
2318
|
+
/**
|
|
2319
|
+
* Record a connector's harness report (from its /v1/ws hello). WARN once
|
|
2320
|
+
* each time the attached harness turns to an inactive veto: such a harness
|
|
2321
|
+
* refuses every turn without a tool grant, so its rooms go quiet.
|
|
2322
|
+
*/
|
|
2323
|
+
recordHarnessReport(report: HarnessReport): void;
|
|
2324
|
+
/** Forget `report` when the connector that sent it disconnects. A report
|
|
2325
|
+
* from a newer connector (a different object) is kept. */
|
|
2326
|
+
clearHarnessReport(report: HarnessReport): void;
|
|
2327
|
+
/** The latest harness report, or null (GET /v1/status). */
|
|
2328
|
+
harnessReport(): HarnessReport | null;
|
|
2299
2329
|
health(): {
|
|
2300
2330
|
status: "ok";
|
|
2301
2331
|
identity_id: string;
|
|
@@ -2344,6 +2374,8 @@ export declare class BridgeAgent {
|
|
|
2344
2374
|
fast_path_deferred: number;
|
|
2345
2375
|
/** `full`-mode turns deferred by the deterministic pre-check (no fast model call). */
|
|
2346
2376
|
fast_path_intent_deferred: number;
|
|
2377
|
+
/** Fast-model replies that voiced inability/lack of tools: never spoken, deferred behind the canned ack instead. */
|
|
2378
|
+
fast_path_refusal_deferred: number;
|
|
2347
2379
|
/** Credential `voice_fast_path_mode`: "full" (default), "ack_only" or "off". */
|
|
2348
2380
|
fast_path_mode: "full" | "ack_only" | "off";
|
|
2349
2381
|
/** Plan 2026-09-06 (agent voice settings): version of the loaded backend
|
|
@@ -2365,6 +2397,12 @@ export declare class BridgeAgent {
|
|
|
2365
2397
|
* agent_sessions stays >=1 means two connectors are flapping the same
|
|
2366
2398
|
* identity (split-brain), not one stable harness. */
|
|
2367
2399
|
evictions: number;
|
|
2400
|
+
/** Harness tool gate H4: is the attached harness's tool veto active?
|
|
2401
|
+
* null when no connector is attached with a report. false means that
|
|
2402
|
+
* harness refuses every turn without a tool grant. Only this boolean is
|
|
2403
|
+
* on the unauthenticated /healthz; name and version are on the
|
|
2404
|
+
* authenticated GET /v1/status. */
|
|
2405
|
+
harness_tool_veto_active: boolean | null;
|
|
2368
2406
|
uptime_s: number;
|
|
2369
2407
|
version: string;
|
|
2370
2408
|
/** M6: is the self-heal sweep running at all? It is the SOLE writer of the
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connector's self-report, carried on its `/v1/ws` hello (harness tool
|
|
3
|
+
* gate, fix round 1 H4): which harness is attached and whether its tool veto
|
|
4
|
+
* is active. Untrusted input from a loopback client: every field is closed to
|
|
5
|
+
* a known set or a bounded version token, so only booleans/versions reach
|
|
6
|
+
* /healthz, GET /v1/status and the service log.
|
|
7
|
+
*/
|
|
8
|
+
export interface HarnessReport {
|
|
9
|
+
readonly name: "hermes" | "openclaw" | "other";
|
|
10
|
+
readonly version: string | null;
|
|
11
|
+
/** "inactive": the harness refuses every turn without a tool grant. */
|
|
12
|
+
readonly tool_veto: "active" | "inactive" | "unknown";
|
|
13
|
+
}
|
|
14
|
+
export declare function parseHarnessReport(raw: unknown): HarnessReport | null;
|
package/dist/index.js
CHANGED
|
@@ -28288,11 +28288,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
|
|
|
28288
28288
|
const marker = "\u2026";
|
|
28289
28289
|
return content.slice(0, Math.max(0, max - marker.length)) + marker;
|
|
28290
28290
|
}
|
|
28291
|
+
var FAST_DEFER_RULE = `Whenever you cannot answer completely from what is shown here, never say you can't, that you lack a tool, access or real-time data: reply with one short sentence like "Let me check that." and end with the exact token [[DEFER]] so the full agent answers next.`;
|
|
28292
|
+
var REFUSAL_PATTERNS = [
|
|
28293
|
+
// "I can't / cannot / am unable to / am not able to <check|access|...>", optional adverb.
|
|
28294
|
+
/\b(?:can't|cannot|can\s+not|unable\s+to|not\s+able\s+to)\s+(?:\w+ly\s+)?(?:check|access|look|see|browse|search|get|provide|give\s+you|find|retrieve|fetch|pull|know|help\s+with\s+(?:that|this)|do\s+that)\b/,
|
|
28295
|
+
// "I don't / do not / doesn't have (a|any|the)? <tool|access|ability|real-time|live ...>".
|
|
28296
|
+
/\b(?:don't|do\s+not|doesn't|does\s+not|lack)\s+(?:have\s+)?(?:a\s+|any\s+|the\s+)?(?:tool|tools|access|ability|capability|real[-\s]?time|live|current|internet|web|way\s+to)\b/,
|
|
28297
|
+
/\bno\s+(?:access|tools?|internet|web\s+access|real[-\s]?time|live\s+data|way\s+to\s+(?:check|access|know|see))\b/,
|
|
28298
|
+
/\bas\s+an\s+(?:ai|a\.i\.|language\s+model)\b/,
|
|
28299
|
+
/\bbeyond\s+my\s+(?:capabilities|abilities|capability)\b/,
|
|
28300
|
+
/\b(?:knowledge\s+cutoff|training\s+data)\b/
|
|
28301
|
+
];
|
|
28302
|
+
function isRefusal(reply) {
|
|
28303
|
+
const t = reply.toLowerCase().replace(/[‘’]/g, "'");
|
|
28304
|
+
return REFUSAL_PATTERNS.some((re) => re.test(t));
|
|
28305
|
+
}
|
|
28291
28306
|
function buildFastMessages(args2) {
|
|
28292
28307
|
const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
|
|
28293
28308
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
28294
28309
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
28295
|
-
const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
|
|
28310
|
+
const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
|
|
28296
28311
|
const recent = args2.memory.recent.map((t) => ({
|
|
28297
28312
|
role: t.role === "agent" ? "assistant" : "user",
|
|
28298
28313
|
content: t.text
|
|
@@ -28623,7 +28638,7 @@ function assertManagedName(name) {
|
|
|
28623
28638
|
}
|
|
28624
28639
|
function isLocalOnlyName(name) {
|
|
28625
28640
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
28626
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json";
|
|
28641
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets";
|
|
28627
28642
|
}
|
|
28628
28643
|
function atomicWrite(path, body, deps) {
|
|
28629
28644
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -28682,6 +28697,26 @@ function lineHash(line) {
|
|
|
28682
28697
|
return createHash2("sha256").update(line.trim(), "utf8").digest("hex");
|
|
28683
28698
|
}
|
|
28684
28699
|
var LOCAL_REPORT_FILE = "local-config.json";
|
|
28700
|
+
var SECRET_PATH_COUNT_MAX = 1e4;
|
|
28701
|
+
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= SECRET_PATH_COUNT_MAX;
|
|
28702
|
+
function parseSecretPathsReport(raw) {
|
|
28703
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
28704
|
+
const r = raw;
|
|
28705
|
+
const f = r.outside_by_field;
|
|
28706
|
+
if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite)) return void 0;
|
|
28707
|
+
return {
|
|
28708
|
+
mode: r.mode,
|
|
28709
|
+
inside: r.inside,
|
|
28710
|
+
allowlisted: r.allowlisted,
|
|
28711
|
+
outside: r.outside,
|
|
28712
|
+
outside_by_field: {
|
|
28713
|
+
headers_file: f.headers_file,
|
|
28714
|
+
postgres_url_file: f.postgres_url_file,
|
|
28715
|
+
search_key_file: f.search_key_file,
|
|
28716
|
+
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {}
|
|
28717
|
+
}
|
|
28718
|
+
};
|
|
28719
|
+
}
|
|
28685
28720
|
var LEGACY_MEMORY_SOURCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
28686
28721
|
var LEGACY_MEMORY_REPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
28687
28722
|
var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
|
|
@@ -28750,7 +28785,8 @@ function readLocalReport(home, identityId) {
|
|
|
28750
28785
|
persona,
|
|
28751
28786
|
has_search_key: hasSearchKey,
|
|
28752
28787
|
has_llm_key: hasLlmKey,
|
|
28753
|
-
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source)
|
|
28788
|
+
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
28789
|
+
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
28754
28790
|
};
|
|
28755
28791
|
}
|
|
28756
28792
|
function deleteLegacyMemorySources(home, identityId, ids) {
|
|
@@ -29552,6 +29588,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29552
29588
|
fastPathDeferred = 0;
|
|
29553
29589
|
/** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
|
|
29554
29590
|
fastPathIntentDeferred = 0;
|
|
29591
|
+
/** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
|
|
29592
|
+
fastPathRefusalDeferred = 0;
|
|
29555
29593
|
/**
|
|
29556
29594
|
* Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
|
|
29557
29595
|
* verb can tell "bridge process up" (the app's green dot, which only
|
|
@@ -29946,7 +29984,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
29946
29984
|
// Inert legacy files only. `readLocalReport` revalidates the current
|
|
29947
29985
|
// regular-file bytes and strips every local path before this leaves the
|
|
29948
29986
|
// machine.
|
|
29949
|
-
legacy_memory: local?.legacy_memory ?? []
|
|
29987
|
+
legacy_memory: local?.legacy_memory ?? [],
|
|
29988
|
+
// §B.4: counts of server-named secret paths inside / allowlisted /
|
|
29989
|
+
// outside the secrets directory, and the runtime's mode. Numbers and
|
|
29990
|
+
// one enum only; readLocalReport rebuilds it so no path rides along.
|
|
29991
|
+
secret_paths: local?.secret_paths ?? null
|
|
29950
29992
|
};
|
|
29951
29993
|
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
29952
29994
|
method: "POST",
|
|
@@ -31829,11 +31871,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
31829
31871
|
});
|
|
31830
31872
|
const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
|
|
31831
31873
|
const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
|
|
31832
|
-
const
|
|
31833
|
-
if (!reply) return "failed";
|
|
31874
|
+
const split3 = splitFastReply(raw, 200, maxSentences);
|
|
31875
|
+
if (!split3.reply) return "failed";
|
|
31876
|
+
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
31877
|
+
const defer = split3.defer || refusal;
|
|
31878
|
+
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
31834
31879
|
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
31835
31880
|
this.fastPathSent++;
|
|
31836
31881
|
if (defer) this.fastPathDeferred++;
|
|
31882
|
+
if (refusal) this.fastPathRefusalDeferred++;
|
|
31837
31883
|
return defer ? "deferred" : "answered";
|
|
31838
31884
|
} catch (e) {
|
|
31839
31885
|
this.fastPathFailed++;
|
|
@@ -32170,14 +32216,25 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
32170
32216
|
*
|
|
32171
32217
|
* Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
|
|
32172
32218
|
* the kill switch is on, the channel is a DM, and EITHER
|
|
32173
|
-
* (a) audience is owner_dm AND the sender is the pinned
|
|
32174
|
-
* (`channelOwner`, never "anomalous")
|
|
32175
|
-
*
|
|
32219
|
+
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
32220
|
+
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
32221
|
+
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
32222
|
+
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
32223
|
+
* LIMIT below); OR
|
|
32176
32224
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
32177
32225
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
32178
32226
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
32179
32227
|
* owner. A speaker claim from any non-relay sender gets nothing.
|
|
32180
32228
|
* Everything else gets the frozen empty list.
|
|
32229
|
+
*
|
|
32230
|
+
* LIMIT (harness gate review H2, 2026-10-05): "pinned structural owner"
|
|
32231
|
+
* is PER CHANNEL. resolveAndPinOwner pins the single human counterpart of
|
|
32232
|
+
* ANY DM, so in a DM with member Bob, Bob is that channel's pin. What
|
|
32233
|
+
* separates Bob from the real owner is only the server's `is_agent_owner`
|
|
32234
|
+
* flag (connect_channels.py: counterpart user == agent's creator). A
|
|
32235
|
+
* backend that sets that flag wrongly gives Bob the grants, and a harness
|
|
32236
|
+
* connector turns grants into its full toolset. Binding the owner identity
|
|
32237
|
+
* once, outside server control, is an open design follow-up.
|
|
32181
32238
|
*/
|
|
32182
32239
|
authorizationFor(channelId, senderId, relaySpeakerId) {
|
|
32183
32240
|
const meta = this.freshMeta(channelId);
|
|
@@ -33723,6 +33780,31 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33723
33780
|
noteSessionEvicted() {
|
|
33724
33781
|
this.evictions++;
|
|
33725
33782
|
}
|
|
33783
|
+
/** Harness tool gate H4: latest connector self-report (see harness-report.ts). */
|
|
33784
|
+
harness = null;
|
|
33785
|
+
/**
|
|
33786
|
+
* Record a connector's harness report (from its /v1/ws hello). WARN once
|
|
33787
|
+
* each time the attached harness turns to an inactive veto: such a harness
|
|
33788
|
+
* refuses every turn without a tool grant, so its rooms go quiet.
|
|
33789
|
+
*/
|
|
33790
|
+
recordHarnessReport(report) {
|
|
33791
|
+
const wasInactive = this.harness?.tool_veto === "inactive";
|
|
33792
|
+
this.harness = report;
|
|
33793
|
+
if (report.tool_veto === "inactive" && !wasInactive) {
|
|
33794
|
+
writeServiceLog2("warn", "harness.tool_veto_inactive", {
|
|
33795
|
+
harness: ServiceLogEnum.of(report.name, ["hermes", "openclaw", "other"])
|
|
33796
|
+
});
|
|
33797
|
+
}
|
|
33798
|
+
}
|
|
33799
|
+
/** Forget `report` when the connector that sent it disconnects. A report
|
|
33800
|
+
* from a newer connector (a different object) is kept. */
|
|
33801
|
+
clearHarnessReport(report) {
|
|
33802
|
+
if (this.harness === report) this.harness = null;
|
|
33803
|
+
}
|
|
33804
|
+
/** The latest harness report, or null (GET /v1/status). */
|
|
33805
|
+
harnessReport() {
|
|
33806
|
+
return this.harness;
|
|
33807
|
+
}
|
|
33726
33808
|
health() {
|
|
33727
33809
|
let joined = 0;
|
|
33728
33810
|
for (const s of this.joinState.values()) if (s === "JOINED") joined++;
|
|
@@ -33785,12 +33867,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
33785
33867
|
fast_path_skipped: this.fastPathSkipped,
|
|
33786
33868
|
fast_path_deferred: this.fastPathDeferred,
|
|
33787
33869
|
fast_path_intent_deferred: this.fastPathIntentDeferred,
|
|
33870
|
+
fast_path_refusal_deferred: this.fastPathRefusalDeferred,
|
|
33788
33871
|
fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
|
|
33789
33872
|
voice_settings_version: this.voiceSettingsVersion,
|
|
33790
33873
|
voice_settings_source: this.voiceSettingsSource,
|
|
33791
33874
|
delivery_pings: this.pingsReceived,
|
|
33792
33875
|
agent_sessions: this.authSessions,
|
|
33793
33876
|
evictions: this.evictions,
|
|
33877
|
+
harness_tool_veto_active: this.harness === null ? null : this.harness.tool_veto === "active",
|
|
33794
33878
|
uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
|
|
33795
33879
|
version: BRIDGE_VERSION,
|
|
33796
33880
|
...this.memory.stats?.() ?? {}
|
|
@@ -34323,6 +34407,9 @@ function createHttpServer(agent, opts) {
|
|
|
34323
34407
|
const r = await agent.vaultDelete(parsed.data.enclave_id, parsed.data.vault_channel_id, parsed.data.node_id);
|
|
34324
34408
|
return json(res, 200, r);
|
|
34325
34409
|
}
|
|
34410
|
+
if (method === "GET" && path === "/v1/status") {
|
|
34411
|
+
return json(res, 200, { harness: agent.harnessReport() });
|
|
34412
|
+
}
|
|
34326
34413
|
if (method === "POST" && path === "/v1/status") {
|
|
34327
34414
|
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
34328
34415
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
@@ -34412,6 +34499,17 @@ function createHttpServer(agent, opts) {
|
|
|
34412
34499
|
return { server, closeAllSseStreams: () => closeSse() };
|
|
34413
34500
|
}
|
|
34414
34501
|
|
|
34502
|
+
// src/harness-report.ts
|
|
34503
|
+
var VERSION_TOKEN = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,39}$/;
|
|
34504
|
+
function parseHarnessReport(raw) {
|
|
34505
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
34506
|
+
const r = raw;
|
|
34507
|
+
const name = r.name === "hermes" || r.name === "openclaw" ? r.name : "other";
|
|
34508
|
+
const version = typeof r.version === "string" && VERSION_TOKEN.test(r.version) ? r.version : null;
|
|
34509
|
+
const tool_veto = r.tool_veto === "active" || r.tool_veto === "inactive" ? r.tool_veto : "unknown";
|
|
34510
|
+
return Object.freeze({ name, version, tool_veto });
|
|
34511
|
+
}
|
|
34512
|
+
|
|
34415
34513
|
// src/ws-server.ts
|
|
34416
34514
|
import { WebSocketServer, WebSocket } from "ws";
|
|
34417
34515
|
var PING_INTERVAL_MS = 3e4;
|
|
@@ -34542,6 +34640,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
34542
34640
|
}
|
|
34543
34641
|
delivered.clear();
|
|
34544
34642
|
if (current?.sock === sock) current = null;
|
|
34643
|
+
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
34545
34644
|
};
|
|
34546
34645
|
current = { sock, cleanup, sweepStale };
|
|
34547
34646
|
sock.on("close", cleanup);
|
|
@@ -34604,7 +34703,11 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
34604
34703
|
return;
|
|
34605
34704
|
}
|
|
34606
34705
|
if (m.type === "hello") {
|
|
34607
|
-
|
|
34706
|
+
const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
|
|
34707
|
+
if (report) {
|
|
34708
|
+
session.harness = report;
|
|
34709
|
+
agent.recordHarnessReport(report);
|
|
34710
|
+
} else session.consumer = "runtime";
|
|
34608
34711
|
sendJson(sock, { type: "hello", features: ["nack"] });
|
|
34609
34712
|
return;
|
|
34610
34713
|
}
|
|
@@ -35,6 +35,28 @@ export interface LocalReport {
|
|
|
35
35
|
has_search_key: boolean;
|
|
36
36
|
has_llm_key: boolean;
|
|
37
37
|
legacy_memory: LegacyMemoryReport[];
|
|
38
|
+
/** §B.4 counts; undefined when the runtime reported none or the block was malformed. */
|
|
39
|
+
secret_paths?: SecretPathsReport;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* §B.4: how many server-named secret paths in the managed config are inside
|
|
43
|
+
* the secrets directory, owner-allowlisted, or outside it, and the runtime's
|
|
44
|
+
* mode. Rebuilt field by field from the runtime's file: numbers and one enum,
|
|
45
|
+
* so no path (the owner's `allowed_secret_paths` sits in the same file) can
|
|
46
|
+
* ride along to the backend.
|
|
47
|
+
*/
|
|
48
|
+
export interface SecretPathsReport {
|
|
49
|
+
mode: "report" | "enforce";
|
|
50
|
+
inside: number;
|
|
51
|
+
allowlisted: number;
|
|
52
|
+
outside: number;
|
|
53
|
+
/** `sqlite` (H1) is absent from a runtime that predates it; absent means unknown, not zero. */
|
|
54
|
+
outside_by_field: {
|
|
55
|
+
headers_file: number;
|
|
56
|
+
postgres_url_file: number;
|
|
57
|
+
search_key_file: number;
|
|
58
|
+
sqlite?: number;
|
|
59
|
+
};
|
|
38
60
|
}
|
|
39
61
|
export type LegacyMemoryKind = "global-memory" | "conversation-history" | "captured-memory";
|
|
40
62
|
export declare const LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.26.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.26.5";
|
|
@@ -29,6 +29,15 @@ export declare const MAX_MESSAGE_CHARS = 8000;
|
|
|
29
29
|
export declare const FAST_PATH_MEMORY_TURNS = 10;
|
|
30
30
|
export declare const FAST_RECENT_CHARS_CAP = 1500;
|
|
31
31
|
export declare const FAST_FACTS_CHARS_CAP = 2000;
|
|
32
|
+
/**
|
|
33
|
+
* Live bug 2026-10-02 (bridge 0.26.3): asked for Sydney's weather, the fast
|
|
34
|
+
* model said "I can't check the weather right now since I don't have a
|
|
35
|
+
* tool..." with no [[DEFER]], so the runtime (which has web_search) never got
|
|
36
|
+
* the turn. Placed last before the continuity clause so it is the final rule
|
|
37
|
+
* the model reads. Compiled in, never interpolated with peer data.
|
|
38
|
+
*/
|
|
39
|
+
export declare const FAST_DEFER_RULE: string;
|
|
40
|
+
export declare function isRefusal(reply: string): boolean;
|
|
32
41
|
export declare function buildFastMessages(args: {
|
|
33
42
|
persona: VoicePersona;
|
|
34
43
|
guidance: string;
|