switchroom 0.14.76 → 0.14.78
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/bin/user-profile-refresh-hook.sh +32 -8
- package/dist/cli/switchroom.js +121 -24
- package/package.json +1 -1
- package/profiles/default/workspace/CLAUDE.md.hbs +11 -13
- package/telegram-plugin/dist/gateway/gateway.js +5 -5
- package/telegram-plugin/hooks/secret-guard-pretool.mjs +10 -1
- package/telegram-plugin/tests/secret-guard-pretool.test.ts +7 -1
|
@@ -18,21 +18,45 @@ if [ -f "$MARKER" ]; then
|
|
|
18
18
|
fi
|
|
19
19
|
# Fire-and-forget refresh via MCP. Timeout bounded by Claude Code hook.
|
|
20
20
|
# Output nothing on success or failure; errors are surfaced via doctor.
|
|
21
|
+
#
|
|
22
|
+
# Two bugs this fixes (2026-06-07):
|
|
23
|
+
# 1. The Hindsight server is STATELESS (HINDSIGHT_API_MCP_STATELESS=true) and
|
|
24
|
+
# returns NO mcp-session-id header, so the old code's `[ -n "$SESSION" ]`
|
|
25
|
+
# gate was ALWAYS false → the refresh NEVER fired. Stateless MCP needs no
|
|
26
|
+
# session — drop the gate (mirrors src/memory/hindsight.ts, which tolerates
|
|
27
|
+
# a missing session id).
|
|
28
|
+
# 2. refresh_mental_model takes `mental_model_id` (a UUID), NOT `name`. Passing
|
|
29
|
+
# `name` returns isError "Missing required argument: mental_model_id". So we
|
|
30
|
+
# list_mental_models, resolve the `user-profile` model's id, then refresh
|
|
31
|
+
# by id.
|
|
21
32
|
{
|
|
22
|
-
#
|
|
23
|
-
|
|
33
|
+
# initialize (no session id needed on the stateless server).
|
|
34
|
+
curl -sS -X POST "$API_URL/mcp/" \
|
|
24
35
|
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
|
|
25
|
-
-H "X-Bank-Id: $BANK_ID"
|
|
36
|
+
-H "X-Bank-Id: $BANK_ID" \
|
|
26
37
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"mm-refresh","version":"0.1"}}}' \
|
|
27
|
-
-m 3 -o /dev/null 2>/dev/null
|
|
28
|
-
|
|
38
|
+
-m 3 -o /dev/null 2>/dev/null || true
|
|
39
|
+
|
|
40
|
+
# Resolve the user-profile mental model's id (refresh is by id, not name).
|
|
41
|
+
# list_mental_models returns the items as a JSON STRING nested in
|
|
42
|
+
# .result.structuredContent.result, so parse with jq (fromjson). The agent
|
|
43
|
+
# image ships jq; if it's somehow absent, MM_ID stays empty → skip (the next
|
|
44
|
+
# Stop fires it again; fire-and-forget, no wedge).
|
|
45
|
+
MM_ID=$(curl -sS -X POST "$API_URL/mcp/" \
|
|
46
|
+
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
|
|
47
|
+
-H "X-Bank-Id: $BANK_ID" \
|
|
48
|
+
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_mental_models","arguments":{}}}' \
|
|
49
|
+
-m 5 2>/dev/null \
|
|
50
|
+
| grep '^data:' | sed 's/^data: //' \
|
|
51
|
+
| jq -r '.result.structuredContent.result | fromjson | .items[] | select(.name=="user-profile") | .id' 2>/dev/null | head -1)
|
|
52
|
+
|
|
53
|
+
if [ -n "$MM_ID" ]; then
|
|
29
54
|
curl -sS -X POST "$API_URL/mcp/" \
|
|
30
55
|
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
|
|
31
|
-
-H "X-Bank-Id: $BANK_ID"
|
|
32
|
-
-d
|
|
56
|
+
-H "X-Bank-Id: $BANK_ID" \
|
|
57
|
+
-d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"refresh_mental_model\",\"arguments\":{\"mental_model_id\":\"$MM_ID\"}}}" \
|
|
33
58
|
-m 5 -o /dev/null 2>/dev/null || true
|
|
34
59
|
fi
|
|
35
|
-
rm -f /tmp/mm-refresh-$$.headers 2>/dev/null || true
|
|
36
60
|
date +%s > "$MARKER"
|
|
37
61
|
} &
|
|
38
62
|
exit 0
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -14987,7 +14987,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
|
14987
14987
|
name: "create_mental_model",
|
|
14988
14988
|
arguments: {
|
|
14989
14989
|
name: "user-profile",
|
|
14990
|
-
|
|
14990
|
+
source_query: "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.",
|
|
14991
14991
|
types: ["world", "experience"]
|
|
14992
14992
|
}
|
|
14993
14993
|
}
|
|
@@ -14998,6 +14998,13 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
|
14998
14998
|
if (!createResponse.ok) {
|
|
14999
14999
|
return { ok: false, reason: `Create MM HTTP ${createResponse.status}` };
|
|
15000
15000
|
}
|
|
15001
|
+
try {
|
|
15002
|
+
const created = await parseSseOrJson(createResponse);
|
|
15003
|
+
if (created.result?.isError === true) {
|
|
15004
|
+
const msg = created.result.content?.[0]?.text ?? "create returned isError";
|
|
15005
|
+
return { ok: false, reason: msg };
|
|
15006
|
+
}
|
|
15007
|
+
} catch {}
|
|
15001
15008
|
return { ok: true };
|
|
15002
15009
|
} catch (err) {
|
|
15003
15010
|
if (err.name === "AbortError") {
|
|
@@ -15204,6 +15211,13 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
|
|
|
15204
15211
|
if (!toolResponse.ok) {
|
|
15205
15212
|
return { ok: false, reason: `Tool call HTTP ${toolResponse.status}` };
|
|
15206
15213
|
}
|
|
15214
|
+
try {
|
|
15215
|
+
const parsed = await parseSseOrJson(toolResponse);
|
|
15216
|
+
if (parsed.result?.isError === true) {
|
|
15217
|
+
const msg = parsed.result.content?.[0]?.text ?? "tool returned isError";
|
|
15218
|
+
return { ok: false, reason: msg };
|
|
15219
|
+
}
|
|
15220
|
+
} catch {}
|
|
15207
15221
|
return { ok: true };
|
|
15208
15222
|
} catch (err) {
|
|
15209
15223
|
if (err.name === "AbortError") {
|
|
@@ -20876,7 +20890,7 @@ function getHindsightSettingsEntry(agentName, config) {
|
|
|
20876
20890
|
}
|
|
20877
20891
|
const collection = getCollectionForAgent(agentName, config);
|
|
20878
20892
|
const mcpConfig = generateHindsightMcpConfig(collection, memoryConfig);
|
|
20879
|
-
return { key: "hindsight", value: mcpConfig };
|
|
20893
|
+
return { key: "hindsight", value: { ...mcpConfig, alwaysLoad: true } };
|
|
20880
20894
|
}
|
|
20881
20895
|
function getPlaywrightMcpSettingsEntry() {
|
|
20882
20896
|
return {
|
|
@@ -49681,8 +49695,8 @@ var {
|
|
|
49681
49695
|
} = import__.default;
|
|
49682
49696
|
|
|
49683
49697
|
// src/build-info.ts
|
|
49684
|
-
var VERSION = "0.14.
|
|
49685
|
-
var COMMIT_SHA = "
|
|
49698
|
+
var VERSION = "0.14.78";
|
|
49699
|
+
var COMMIT_SHA = "ac0dae98";
|
|
49686
49700
|
|
|
49687
49701
|
// src/cli/agent.ts
|
|
49688
49702
|
init_source();
|
|
@@ -50534,16 +50548,16 @@ Every turn that answers a user message ends with a user-visible
|
|
|
50534
50548
|
sees; your terminal output never reaches them.`;
|
|
50535
50549
|
var MEMORY_GUIDANCE = `## Memory \u2014 proactive, conversational
|
|
50536
50550
|
|
|
50537
|
-
You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`
|
|
50551
|
+
You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_document\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
|
|
50538
50552
|
|
|
50539
50553
|
### Retain proactively
|
|
50540
50554
|
When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
|
|
50541
50555
|
|
|
50542
50556
|
### Correct proactively
|
|
50543
|
-
When the user corrects you or contradicts a prior memory, call \`
|
|
50557
|
+
When the user corrects you or contradicts a prior memory, call \`delete_document\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
|
|
50544
50558
|
|
|
50545
50559
|
### Forget proactively
|
|
50546
|
-
When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`
|
|
50560
|
+
When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_document\` for matching entries and confirm what was removed.
|
|
50547
50561
|
|
|
50548
50562
|
### Inspect proactively
|
|
50549
50563
|
When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
|
|
@@ -50600,7 +50614,18 @@ name from that list.
|
|
|
50600
50614
|
Only call the \`vault_request_access\` MCP tool \u2014 which pings the
|
|
50601
50615
|
operator for a Telegram approval \u2014 for a key you've **confirmed via
|
|
50602
50616
|
\`vault list\` you don't already have.** Requesting access to a guessed
|
|
50603
|
-
or already-held key wastes the operator's tap and fails
|
|
50617
|
+
or already-held key wastes the operator's tap and fails.
|
|
50618
|
+
|
|
50619
|
+
**Never put a secret's value \u2014 or a \`vault:<key>\` placeholder \u2014 in a
|
|
50620
|
+
tool call.** Tool arguments are sent to the underlying API verbatim;
|
|
50621
|
+
nothing resolves a \`vault:\` reference for them. A real secret never
|
|
50622
|
+
needs to appear in a tool call \u2014 the launcher injects it into the
|
|
50623
|
+
tool's environment for you. A value you genuinely must pass literally
|
|
50624
|
+
(an account or customer ID) is a public identifier, not a secret, and
|
|
50625
|
+
belongs in plain config, not the vault. If a tool call is **blocked for
|
|
50626
|
+
containing a vaulted secret**, report that exact block message to the
|
|
50627
|
+
operator and stop \u2014 don't theorise about auth or tokens; it just means
|
|
50628
|
+
a stored value reached a tool argument.`;
|
|
50604
50629
|
function renderFleetInvariants() {
|
|
50605
50630
|
return [
|
|
50606
50631
|
"<!--",
|
|
@@ -51111,6 +51136,11 @@ function buildHumanizerEnvVars(agentDir, agent) {
|
|
|
51111
51136
|
const resolved = expanded.startsWith("/") ? expanded : resolve11(agentDir, expanded);
|
|
51112
51137
|
return { HUMANIZER_VOICE_FILE: resolved };
|
|
51113
51138
|
}
|
|
51139
|
+
function buildToolSearchEnvVars() {
|
|
51140
|
+
if (process.env.SWITCHROOM_DISABLE_TOOL_SEARCH === "1")
|
|
51141
|
+
return {};
|
|
51142
|
+
return { ENABLE_TOOL_SEARCH: process.env.SWITCHROOM_TOOL_SEARCH_MODE ?? "auto" };
|
|
51143
|
+
}
|
|
51114
51144
|
var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
|
|
51115
51145
|
"permissions",
|
|
51116
51146
|
"mcpServers",
|
|
@@ -51440,6 +51470,7 @@ function buildWorkspaceContext(args) {
|
|
|
51440
51470
|
fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
|
|
51441
51471
|
userEnvQuoted: (() => {
|
|
51442
51472
|
const combined = {
|
|
51473
|
+
...buildToolSearchEnvVars(),
|
|
51443
51474
|
...channelsToEnv(agentConfig),
|
|
51444
51475
|
...agentConfig.env ?? {},
|
|
51445
51476
|
...buildRepoEnvVars(name, agentDir, agentConfig),
|
|
@@ -51750,7 +51781,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
51750
51781
|
TELEGRAM_STATE_DIR: telegramStateDir,
|
|
51751
51782
|
SWITCHROOM_CONFIG: resolvedConfigPath,
|
|
51752
51783
|
SWITCHROOM_CLI_PATH: switchroomCliPath
|
|
51753
|
-
}
|
|
51784
|
+
},
|
|
51785
|
+
alwaysLoad: true
|
|
51754
51786
|
},
|
|
51755
51787
|
"agent-config": {
|
|
51756
51788
|
command: switchroomCliPath,
|
|
@@ -51758,7 +51790,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
51758
51790
|
env: {
|
|
51759
51791
|
SWITCHROOM_AGENT_NAME: name,
|
|
51760
51792
|
SWITCHROOM_CONFIG: resolvedConfigPath
|
|
51761
|
-
}
|
|
51793
|
+
},
|
|
51794
|
+
alwaysLoad: true
|
|
51762
51795
|
}
|
|
51763
51796
|
};
|
|
51764
51797
|
if (agentConfig.admin === true) {
|
|
@@ -52503,6 +52536,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
52503
52536
|
fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
|
|
52504
52537
|
userEnvQuoted: (() => {
|
|
52505
52538
|
const combined = {
|
|
52539
|
+
...buildToolSearchEnvVars(),
|
|
52506
52540
|
...channelsToEnv(agentConfig),
|
|
52507
52541
|
...agentConfig.env ?? {},
|
|
52508
52542
|
...buildRepoEnvVars(name, agentDir, agentConfig)
|
|
@@ -52785,7 +52819,8 @@ ${body}
|
|
|
52785
52819
|
TELEGRAM_STATE_DIR: telegramStateDir,
|
|
52786
52820
|
SWITCHROOM_CONFIG: resolvedConfigPath,
|
|
52787
52821
|
SWITCHROOM_CLI_PATH: switchroomCliPath
|
|
52788
|
-
}
|
|
52822
|
+
},
|
|
52823
|
+
alwaysLoad: true
|
|
52789
52824
|
},
|
|
52790
52825
|
"agent-config": {
|
|
52791
52826
|
command: switchroomCliPath,
|
|
@@ -52793,7 +52828,8 @@ ${body}
|
|
|
52793
52828
|
env: {
|
|
52794
52829
|
SWITCHROOM_AGENT_NAME: name,
|
|
52795
52830
|
SWITCHROOM_CONFIG: resolvedConfigPath
|
|
52796
|
-
}
|
|
52831
|
+
},
|
|
52832
|
+
alwaysLoad: true
|
|
52797
52833
|
}
|
|
52798
52834
|
};
|
|
52799
52835
|
if (agentConfig.admin === true) {
|
|
@@ -59152,9 +59188,8 @@ function makeHttpHindsightClient(apiUrl, opts) {
|
|
|
59152
59188
|
});
|
|
59153
59189
|
return { items: res.items ?? [], total: res.total ?? 0 };
|
|
59154
59190
|
},
|
|
59155
|
-
async deleteMemory(
|
|
59156
|
-
|
|
59157
|
-
await callTool(bankId, sid, "delete_memory", { bank_id: bankId, memory_id: memoryId });
|
|
59191
|
+
async deleteMemory(_bankId, _memoryId) {
|
|
59192
|
+
throw new Error("hindsight exposes no single-memory delete API (delete_memory is not a " + "real tool; a memory id is not a document_id) \u2014 matched secrets cannot " + "be auto-scrubbed; redact manually via the bank UI");
|
|
59158
59193
|
}
|
|
59159
59194
|
};
|
|
59160
59195
|
}
|
|
@@ -59184,11 +59219,18 @@ ${mem.context ?? ""}`;
|
|
|
59184
59219
|
}
|
|
59185
59220
|
let deleted = 0;
|
|
59186
59221
|
if (!opts.dryRun) {
|
|
59222
|
+
let warnedOnce = false;
|
|
59187
59223
|
for (const m of matched) {
|
|
59188
59224
|
try {
|
|
59189
59225
|
await client.deleteMemory(bankId, m.id);
|
|
59190
59226
|
deleted++;
|
|
59191
|
-
} catch {
|
|
59227
|
+
} catch (err) {
|
|
59228
|
+
if (!warnedOnce) {
|
|
59229
|
+
warnedOnce = true;
|
|
59230
|
+
process.stderr.write(`vault-sweep: ${matched.length} secret(s) matched in bank '${bankId}' but ` + `could NOT be auto-deleted: ${err.message}
|
|
59231
|
+
`);
|
|
59232
|
+
}
|
|
59233
|
+
}
|
|
59192
59234
|
}
|
|
59193
59235
|
}
|
|
59194
59236
|
return { matched, deleted };
|
|
@@ -64069,6 +64111,15 @@ import { existsSync as existsSync37 } from "node:fs";
|
|
|
64069
64111
|
|
|
64070
64112
|
// src/vault/doctor.ts
|
|
64071
64113
|
var SENSITIVE_KEY_RE = /oauth(?![a-zA-Z])|token(?![a-zA-Z])|secret(?![a-zA-Z])|api[-_]?key(?![a-zA-Z])|password(?![a-zA-Z])/i;
|
|
64114
|
+
function looksLikeIdentifierValue(value) {
|
|
64115
|
+
const v = value.trim();
|
|
64116
|
+
if (v.length < 8 || v.length > 24)
|
|
64117
|
+
return false;
|
|
64118
|
+
if (!/^[0-9][0-9 -]*[0-9]$/.test(v))
|
|
64119
|
+
return false;
|
|
64120
|
+
const digitCount = v.replace(/[^0-9]/g, "").length;
|
|
64121
|
+
return digitCount >= 8;
|
|
64122
|
+
}
|
|
64072
64123
|
function analyseVaultHealth(input) {
|
|
64073
64124
|
const diagnostics = [];
|
|
64074
64125
|
if (input.brokerConfigured && input.brokerRunning === false) {
|
|
@@ -64124,6 +64175,23 @@ ${keyList}`,
|
|
|
64124
64175
|
fix: "Consider `switchroom vault set <key> --allow <agent>` to restrict access"
|
|
64125
64176
|
});
|
|
64126
64177
|
}
|
|
64178
|
+
const identifierKeys = [];
|
|
64179
|
+
for (const [keyName, entry] of Object.entries(input.vaultKeys)) {
|
|
64180
|
+
if (entry.looksLikeIdentifier)
|
|
64181
|
+
identifierKeys.push(keyName);
|
|
64182
|
+
}
|
|
64183
|
+
if (identifierKeys.length > 0) {
|
|
64184
|
+
const keyList = identifierKeys.map((k) => ` ${k}`).join(`
|
|
64185
|
+
`);
|
|
64186
|
+
diagnostics.push({
|
|
64187
|
+
level: "warn",
|
|
64188
|
+
check: "identifier-stored-as-secret",
|
|
64189
|
+
message: `${identifierKeys.length} vault key(s) hold an identifier-like value (digits only):
|
|
64190
|
+
${keyList}
|
|
64191
|
+
` + `If an agent must pass one of these in a tool call, the secret-guard hook will block it \u2014 it cannot tell a public ID from a secret.`,
|
|
64192
|
+
fix: "If the value is a public identifier (account/customer ID), remove it from the vault and pass it as plain config (e.g. hardcode it in the MCP launcher). If it is genuinely secret, ignore this."
|
|
64193
|
+
});
|
|
64194
|
+
}
|
|
64127
64195
|
const unreferencedKeys = [];
|
|
64128
64196
|
for (const keyName of Object.keys(input.vaultKeys)) {
|
|
64129
64197
|
if (!referencedKeys.has(keyName)) {
|
|
@@ -64215,7 +64283,8 @@ function registerVaultDoctorCommand(vault, program3) {
|
|
|
64215
64283
|
vaultKeys = {};
|
|
64216
64284
|
for (const [name, entry] of Object.entries(entries)) {
|
|
64217
64285
|
vaultKeys[name] = {
|
|
64218
|
-
scope: "scope" in entry && entry.scope ? entry.scope : undefined
|
|
64286
|
+
scope: "scope" in entry && entry.scope ? entry.scope : undefined,
|
|
64287
|
+
looksLikeIdentifier: entry.kind === "string" ? looksLikeIdentifierValue(entry.value) : false
|
|
64219
64288
|
};
|
|
64220
64289
|
}
|
|
64221
64290
|
} catch (err) {
|
|
@@ -77207,7 +77276,18 @@ function formatBytes(bytes) {
|
|
|
77207
77276
|
return `${bytes.toLocaleString()} bytes`;
|
|
77208
77277
|
}
|
|
77209
77278
|
function estimateTokens(bytes) {
|
|
77210
|
-
return Math.round(bytes /
|
|
77279
|
+
return Math.round(bytes / 3.7);
|
|
77280
|
+
}
|
|
77281
|
+
function readMcpServerNames(agentDir) {
|
|
77282
|
+
const mcpPath = join64(agentDir, ".mcp.json");
|
|
77283
|
+
if (!existsSync64(mcpPath))
|
|
77284
|
+
return [];
|
|
77285
|
+
try {
|
|
77286
|
+
const parsed = JSON.parse(readFileSync56(mcpPath, "utf-8"));
|
|
77287
|
+
return Object.keys(parsed.mcpServers ?? {});
|
|
77288
|
+
} catch {
|
|
77289
|
+
return null;
|
|
77290
|
+
}
|
|
77211
77291
|
}
|
|
77212
77292
|
function sha256(content) {
|
|
77213
77293
|
return createHash13("sha256").update(content).digest("hex").slice(0, 16);
|
|
@@ -77425,14 +77505,31 @@ function registerDebugCommand(program3) {
|
|
|
77425
77505
|
const soulMdBytes = soulMdContent.length;
|
|
77426
77506
|
const perTurnBytes = dynamicResult.concatenated.length;
|
|
77427
77507
|
const userBytes = userMessage?.text.length ?? 0;
|
|
77428
|
-
const
|
|
77429
|
-
|
|
77508
|
+
const fleetDir = join64(agentsDir, "..", "fleet");
|
|
77509
|
+
const fleetInvPath = join64(fleetDir, "switchroom-invariants.md");
|
|
77510
|
+
const fleetClaudePath = join64(fleetDir, "CLAUDE.md");
|
|
77511
|
+
const fleetInvBytes = existsSync64(fleetInvPath) ? readFileSync56(fleetInvPath, "utf-8").length : 0;
|
|
77512
|
+
const fleetClaudeBytes = existsSync64(fleetClaudePath) ? readFileSync56(fleetClaudePath, "utf-8").length : 0;
|
|
77513
|
+
const fleetBytes = fleetInvBytes + fleetClaudeBytes;
|
|
77514
|
+
const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
|
|
77515
|
+
console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
|
|
77430
77516
|
console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
|
|
77431
|
-
console.log(`CLAUDE.md:
|
|
77432
|
-
console.log(`
|
|
77433
|
-
console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached)`);
|
|
77517
|
+
console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
|
|
77518
|
+
console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
|
|
77519
|
+
console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
|
|
77434
77520
|
console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
|
|
77435
|
-
console.log(`
|
|
77521
|
+
console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
|
|
77522
|
+
const mcpServers = readMcpServerNames(agentDir);
|
|
77523
|
+
const mcpCount = mcpServers?.length ?? null;
|
|
77524
|
+
const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
|
|
77525
|
+
const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
|
|
77526
|
+
const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
|
|
77527
|
+
console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
|
|
77528
|
+
if (mcpServers && mcpServers.length > 0) {
|
|
77529
|
+
console.log(` [${mcpServers.join(", ")}]`);
|
|
77530
|
+
}
|
|
77531
|
+
const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
|
|
77532
|
+
console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
|
|
77436
77533
|
const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
|
|
77437
77534
|
const stableHash = sha256(stableCacheInput);
|
|
77438
77535
|
console.log(`Cache stable hash: sha256:${stableHash}`);
|
package/package.json
CHANGED
|
@@ -11,23 +11,21 @@ Before answering anything non-trivial:
|
|
|
11
11
|
|
|
12
12
|
1. Confirm who you are (see `IDENTITY.md` if present).
|
|
13
13
|
2. Confirm who you're talking to (see `USER.md` if present).
|
|
14
|
-
3.
|
|
15
|
-
`
|
|
16
|
-
|
|
14
|
+
3. Recall what matters — your memory is **Hindsight**: call
|
|
15
|
+
`mcp__hindsight__recall` when you need prior context, decisions, or who
|
|
16
|
+
someone is. (Full memory protocol is in your `CLAUDE.md`.)
|
|
17
17
|
4. If tools are configured, `TOOLS.md` captures setup-specific notes
|
|
18
18
|
(host paths, credentials, endpoints) that save you discovery work.
|
|
19
19
|
|
|
20
20
|
Don't ask permission to read these files. They exist so you can use them.
|
|
21
21
|
|
|
22
|
-
## Memory
|
|
22
|
+
## Memory
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
write it down. Future-you is fresh every session. Files are your
|
|
30
|
-
continuity.
|
|
24
|
+
Memory is **Hindsight**, not files — `recall` to read, `sync_retain` to keep a
|
|
25
|
+
fact across sessions, `delete_document` to forget. Claude Code's built-in
|
|
26
|
+
file-based auto-memory is disabled for this agent; do not maintain a `MEMORY.md`
|
|
27
|
+
index or a `memory/` daily-notes directory. Your `CLAUDE.md` has the full
|
|
28
|
+
protocol.
|
|
31
29
|
|
|
32
30
|
## Safety defaults
|
|
33
31
|
|
|
@@ -55,8 +53,8 @@ Failed edits burn your own context and the user's patience. Verify first.
|
|
|
55
53
|
tool exists.
|
|
56
54
|
- Long-running commands: use the background flag and poll, rather than
|
|
57
55
|
blocking.
|
|
58
|
-
- `
|
|
59
|
-
|
|
56
|
+
- `mcp__hindsight__recall` is your interface to past context and decisions;
|
|
57
|
+
recall before asking the user something you may already know.
|
|
60
58
|
|
|
61
59
|
## Working on code repositories
|
|
62
60
|
|
|
@@ -52821,11 +52821,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52821
52821
|
}
|
|
52822
52822
|
|
|
52823
52823
|
// ../src/build-info.ts
|
|
52824
|
-
var VERSION = "0.14.
|
|
52825
|
-
var COMMIT_SHA = "
|
|
52826
|
-
var COMMIT_DATE = "2026-06-
|
|
52827
|
-
var LATEST_PR =
|
|
52828
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
52824
|
+
var VERSION = "0.14.78";
|
|
52825
|
+
var COMMIT_SHA = "ac0dae98";
|
|
52826
|
+
var COMMIT_DATE = "2026-06-07T09:07:56+10:00";
|
|
52827
|
+
var LATEST_PR = null;
|
|
52828
|
+
var COMMITS_AHEAD_OF_TAG = 3;
|
|
52829
52829
|
|
|
52830
52830
|
// gateway/boot-version.ts
|
|
52831
52831
|
function formatRelativeAgo(iso) {
|
|
@@ -194,10 +194,19 @@ async function main() {
|
|
|
194
194
|
}
|
|
195
195
|
const hit = scanToolInput(toolInput, vaultValues)
|
|
196
196
|
if (hit) {
|
|
197
|
+
// The reason is read by the model. It must be CORRECT and actionable:
|
|
198
|
+
// earlier versions said "reference it as vault:<key> instead", which
|
|
199
|
+
// suggested a `vault:` resolver that does not exist for tool arguments
|
|
200
|
+
// — sending agents (and operators) chasing a phantom mechanism. State
|
|
201
|
+
// the two real resolutions instead, and do NOT imply a resolver.
|
|
197
202
|
process.stdout.write(
|
|
198
203
|
JSON.stringify({
|
|
199
204
|
decision: 'block',
|
|
200
|
-
reason:
|
|
205
|
+
reason:
|
|
206
|
+
`Blocked: this tool input contains the value of vault secret '${hit.key}'. ` +
|
|
207
|
+
`Secrets must never appear in a tool call. ` +
|
|
208
|
+
`If '${hit.key}' is a real secret, do not pass it as an argument — the launcher injects it into the tool's environment, so you never type it. ` +
|
|
209
|
+
`If '${hit.key}' is actually a public identifier (e.g. an account or customer ID that must appear in the call), it should not be in the vault — ask the operator to move it to plain config.`,
|
|
201
210
|
}),
|
|
202
211
|
)
|
|
203
212
|
process.exit(0)
|
|
@@ -132,7 +132,13 @@ describe('secret-guard-pretool.mjs (broker-direct)', () => {
|
|
|
132
132
|
})
|
|
133
133
|
expect(r.status).toBe(0)
|
|
134
134
|
expect(r.stdout).toContain('"decision":"block"')
|
|
135
|
-
|
|
135
|
+
// The reason must name the offending key and explain the real fix.
|
|
136
|
+
expect(r.stdout).toContain('gh-token')
|
|
137
|
+
expect(r.stdout).toContain('Secrets must never appear in a tool call')
|
|
138
|
+
// It must NOT resurrect the old, misleading "reference it as vault:<key>"
|
|
139
|
+
// suggestion — there is no vault: resolver for tool arguments, and that
|
|
140
|
+
// advice sent agents down a dead end (see the hook's block-reason note).
|
|
141
|
+
expect(r.stdout).not.toContain('reference it as vault:')
|
|
136
142
|
})
|
|
137
143
|
|
|
138
144
|
it('allows when tool_input contains no vaulted value', async () => {
|