switchroom 0.21.13 → 0.21.15
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/dist/agent-scheduler/index.js +6 -2
- package/dist/auth-broker/index.js +6 -2
- package/dist/cli/notion-write-pretool.mjs +6 -2
- package/dist/cli/switchroom.js +1608 -699
- package/dist/host-control/main.js +22 -16
- package/dist/vault/approvals/kernel-server.js +6 -2
- package/dist/vault/broker/server.js +132 -11
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +9 -0
- package/profiles/_shared/agent-self-service.md.hbs +32 -86
- package/profiles/_shared/vault-protocol.md.hbs +17 -62
- package/profiles/default/CLAUDE.md.hbs +76 -74
- package/skills/switchroom-runtime/SKILL.md +32 -0
- package/telegram-plugin/dist/gateway/gateway.js +10 -6
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +22 -0
- package/vendor/hindsight-memory/scripts/lib/directives.py +63 -7
- package/vendor/hindsight-memory/scripts/lib/watermark.py +27 -0
- package/vendor/hindsight-memory/scripts/recall.py +349 -5
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +4 -12
- package/vendor/hindsight-memory/scripts/retain.py +59 -3
- package/vendor/hindsight-memory/scripts/tests/test_config_retain_tool_calls_env.py +98 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +98 -0
- package/vendor/hindsight-memory/scripts/tests/test_incremental_sweep.py +293 -0
- package/vendor/hindsight-memory/scripts/tests/test_profile_capture_nudge.py +335 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +53 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_query_timestamp.py +376 -0
|
@@ -11149,6 +11149,7 @@ var init_schema = __esm(() => {
|
|
|
11149
11149
|
empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
|
|
11150
11150
|
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
11151
11151
|
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
11152
|
+
profile_capture_nudge: exports_external.boolean().optional().describe("Deterministic operator-profile capture nudge (RFC phase4 P3, serves " + 'the "save memories about him" want). When on (switchroom default ' + "true), the auto-recall hook regex-detects a first-person durable " + 'self-statement by the operator ("I prefer …", "my … is …", "I ' + 'always …", "remind me that I …") and appends a terse advisory to ' + "the turn's context telling the model to persist it with an explicit " + "mcp__hindsight__retain tagged `profile:ken` into the agent's OWN bank " + "(not a shared/cross-agent person bank). Detection is pure regex — the " + "model does the judgment in-session and calls retain itself (no model " + "callsite, no silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
11152
11153
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
11153
11154
|
observation_scopes: ObservationScopesSchema,
|
|
11154
11155
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -11197,7 +11198,8 @@ var init_schema = __esm(() => {
|
|
|
11197
11198
|
}).optional().describe("Auto-recall tuning knobs"),
|
|
11198
11199
|
retain: exports_external.object({
|
|
11199
11200
|
every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
|
|
11200
|
-
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
|
|
11201
|
+
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge."),
|
|
11202
|
+
tool_calls: exports_external.boolean().optional().describe("Whether auto-retain stores assistant tool_use blocks (with their " + "entire input dict) and tool_result content (up to 2000 chars) " + "alongside the human/assistant turns. Vendor + switchroom default " + "is true — the fleet behaves byte-identically until an operator " + "sets this false. Tool inputs/results are a large fraction of " + "retained volume (RFC memory-redesign P4) but also where evidence " + "lives (a commit SHA, a failing test's output, the diff that " + "fixed something), so the tradeoff is real and per-agent. false is " + "not a new mode: sidechains already force it off " + "(subagent_retain.py). Set false to drop tool exhaust from an " + "agent's retains. Cascade: per-field merge (agent wins over " + "default).")
|
|
11201
11203
|
}).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
|
|
11202
11204
|
}).optional();
|
|
11203
11205
|
HookEntrySchema = exports_external.object({
|
|
@@ -11585,6 +11587,7 @@ var init_schema = __esm(() => {
|
|
|
11585
11587
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
11586
11588
|
profile: exports_external.string().optional(),
|
|
11587
11589
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
11590
|
+
profile_capture_nudge: exports_external.boolean().optional(),
|
|
11588
11591
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
11589
11592
|
observation_scopes: ObservationScopesSchema,
|
|
11590
11593
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -11633,7 +11636,8 @@ var init_schema = __esm(() => {
|
|
|
11633
11636
|
}).optional(),
|
|
11634
11637
|
retain: exports_external.object({
|
|
11635
11638
|
every_n_turns: exports_external.number().int().min(1).optional(),
|
|
11636
|
-
overlap_turns: exports_external.number().int().min(0).optional()
|
|
11639
|
+
overlap_turns: exports_external.number().int().min(0).optional(),
|
|
11640
|
+
tool_calls: exports_external.boolean().optional()
|
|
11637
11641
|
}).optional(),
|
|
11638
11642
|
bank_mission: exports_external.string().optional(),
|
|
11639
11643
|
reflect_mission: exports_external.string().optional(),
|
|
@@ -21588,7 +21592,7 @@ function allocateAgentUid(name) {
|
|
|
21588
21592
|
}
|
|
21589
21593
|
|
|
21590
21594
|
// src/build-info.ts
|
|
21591
|
-
var VERSION = "0.21.
|
|
21595
|
+
var VERSION = "0.21.15";
|
|
21592
21596
|
|
|
21593
21597
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21594
21598
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
@@ -29064,6 +29068,21 @@ var ROLLOUT_PROBE_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_PROBE_TIMEOUT_MS", 60 *
|
|
|
29064
29068
|
import { chownSync as chownSync3 } from "node:fs";
|
|
29065
29069
|
import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
|
|
29066
29070
|
init_merge();
|
|
29071
|
+
|
|
29072
|
+
// src/agents/singleton-reconcile.ts
|
|
29073
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
29074
|
+
var CORE_SINGLETON_SERVICES = [
|
|
29075
|
+
"vault-broker",
|
|
29076
|
+
"approval-kernel",
|
|
29077
|
+
"switchroom-auth-broker"
|
|
29078
|
+
];
|
|
29079
|
+
var VOICE_SIDECAR_SERVICE = "voice-sidecar";
|
|
29080
|
+
var SINGLETON_SERVICES = [
|
|
29081
|
+
...CORE_SINGLETON_SERVICES,
|
|
29082
|
+
VOICE_SIDECAR_SERVICE
|
|
29083
|
+
];
|
|
29084
|
+
|
|
29085
|
+
// src/cli/write-compose.ts
|
|
29067
29086
|
var AGENT_IMAGE_TAG_RE = /image:\s*\S*switchroom-agent:(\S+)/;
|
|
29068
29087
|
var BACKUP_SUFFIX = ".bak";
|
|
29069
29088
|
function composeBackupPath(composePath) {
|
|
@@ -29177,19 +29196,6 @@ var MAX_BYTES = 10 * 1024 * 1024;
|
|
|
29177
29196
|
// src/agents/docker-fleet.ts
|
|
29178
29197
|
init_loader();
|
|
29179
29198
|
|
|
29180
|
-
// src/agents/singleton-reconcile.ts
|
|
29181
|
-
var import_yaml4 = __toESM(require_dist(), 1);
|
|
29182
|
-
var CORE_SINGLETON_SERVICES = [
|
|
29183
|
-
"vault-broker",
|
|
29184
|
-
"approval-kernel",
|
|
29185
|
-
"switchroom-auth-broker"
|
|
29186
|
-
];
|
|
29187
|
-
var VOICE_SIDECAR_SERVICE = "voice-sidecar";
|
|
29188
|
-
var SINGLETON_SERVICES = [
|
|
29189
|
-
...CORE_SINGLETON_SERVICES,
|
|
29190
|
-
VOICE_SIDECAR_SERVICE
|
|
29191
|
-
];
|
|
29192
|
-
|
|
29193
29199
|
// src/agents/lifecycle.ts
|
|
29194
29200
|
function containerName(name) {
|
|
29195
29201
|
return `switchroom-${name}`;
|
|
@@ -4589,6 +4589,7 @@ var init_schema = __esm(() => {
|
|
|
4589
4589
|
empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
|
|
4590
4590
|
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
4591
4591
|
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
4592
|
+
profile_capture_nudge: exports_external.boolean().optional().describe("Deterministic operator-profile capture nudge (RFC phase4 P3, serves " + 'the "save memories about him" want). When on (switchroom default ' + "true), the auto-recall hook regex-detects a first-person durable " + 'self-statement by the operator ("I prefer …", "my … is …", "I ' + 'always …", "remind me that I …") and appends a terse advisory to ' + "the turn's context telling the model to persist it with an explicit " + "mcp__hindsight__retain tagged `profile:ken` into the agent's OWN bank " + "(not a shared/cross-agent person bank). Detection is pure regex — the " + "model does the judgment in-session and calls retain itself (no model " + "callsite, no silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
4592
4593
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
4593
4594
|
observation_scopes: ObservationScopesSchema,
|
|
4594
4595
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -4637,7 +4638,8 @@ var init_schema = __esm(() => {
|
|
|
4637
4638
|
}).optional().describe("Auto-recall tuning knobs"),
|
|
4638
4639
|
retain: exports_external.object({
|
|
4639
4640
|
every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
|
|
4640
|
-
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
|
|
4641
|
+
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge."),
|
|
4642
|
+
tool_calls: exports_external.boolean().optional().describe("Whether auto-retain stores assistant tool_use blocks (with their " + "entire input dict) and tool_result content (up to 2000 chars) " + "alongside the human/assistant turns. Vendor + switchroom default " + "is true — the fleet behaves byte-identically until an operator " + "sets this false. Tool inputs/results are a large fraction of " + "retained volume (RFC memory-redesign P4) but also where evidence " + "lives (a commit SHA, a failing test's output, the diff that " + "fixed something), so the tradeoff is real and per-agent. false is " + "not a new mode: sidechains already force it off " + "(subagent_retain.py). Set false to drop tool exhaust from an " + "agent's retains. Cascade: per-field merge (agent wins over " + "default).")
|
|
4641
4643
|
}).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
|
|
4642
4644
|
}).optional();
|
|
4643
4645
|
HookEntrySchema = exports_external.object({
|
|
@@ -5025,6 +5027,7 @@ var init_schema = __esm(() => {
|
|
|
5025
5027
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
5026
5028
|
profile: exports_external.string().optional(),
|
|
5027
5029
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
5030
|
+
profile_capture_nudge: exports_external.boolean().optional(),
|
|
5028
5031
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
5029
5032
|
observation_scopes: ObservationScopesSchema,
|
|
5030
5033
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -5073,7 +5076,8 @@ var init_schema = __esm(() => {
|
|
|
5073
5076
|
}).optional(),
|
|
5074
5077
|
retain: exports_external.object({
|
|
5075
5078
|
every_n_turns: exports_external.number().int().min(1).optional(),
|
|
5076
|
-
overlap_turns: exports_external.number().int().min(0).optional()
|
|
5079
|
+
overlap_turns: exports_external.number().int().min(0).optional(),
|
|
5080
|
+
tool_calls: exports_external.boolean().optional()
|
|
5077
5081
|
}).optional(),
|
|
5078
5082
|
bank_mission: exports_external.string().optional(),
|
|
5079
5083
|
reflect_mission: exports_external.string().optional(),
|
|
@@ -4185,6 +4185,7 @@ var init_schema = __esm(() => {
|
|
|
4185
4185
|
empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
|
|
4186
4186
|
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
4187
4187
|
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
4188
|
+
profile_capture_nudge: exports_external.boolean().optional().describe("Deterministic operator-profile capture nudge (RFC phase4 P3, serves " + 'the "save memories about him" want). When on (switchroom default ' + "true), the auto-recall hook regex-detects a first-person durable " + 'self-statement by the operator ("I prefer …", "my … is …", "I ' + 'always …", "remind me that I …") and appends a terse advisory to ' + "the turn's context telling the model to persist it with an explicit " + "mcp__hindsight__retain tagged `profile:ken` into the agent's OWN bank " + "(not a shared/cross-agent person bank). Detection is pure regex — the " + "model does the judgment in-session and calls retain itself (no model " + "callsite, no silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
4188
4189
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
4189
4190
|
observation_scopes: ObservationScopesSchema,
|
|
4190
4191
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -4233,7 +4234,8 @@ var init_schema = __esm(() => {
|
|
|
4233
4234
|
}).optional().describe("Auto-recall tuning knobs"),
|
|
4234
4235
|
retain: exports_external.object({
|
|
4235
4236
|
every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
|
|
4236
|
-
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
|
|
4237
|
+
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge."),
|
|
4238
|
+
tool_calls: exports_external.boolean().optional().describe("Whether auto-retain stores assistant tool_use blocks (with their " + "entire input dict) and tool_result content (up to 2000 chars) " + "alongside the human/assistant turns. Vendor + switchroom default " + "is true — the fleet behaves byte-identically until an operator " + "sets this false. Tool inputs/results are a large fraction of " + "retained volume (RFC memory-redesign P4) but also where evidence " + "lives (a commit SHA, a failing test's output, the diff that " + "fixed something), so the tradeoff is real and per-agent. false is " + "not a new mode: sidechains already force it off " + "(subagent_retain.py). Set false to drop tool exhaust from an " + "agent's retains. Cascade: per-field merge (agent wins over " + "default).")
|
|
4237
4239
|
}).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
|
|
4238
4240
|
}).optional();
|
|
4239
4241
|
HookEntrySchema = exports_external.object({
|
|
@@ -4621,6 +4623,7 @@ var init_schema = __esm(() => {
|
|
|
4621
4623
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
4622
4624
|
profile: exports_external.string().optional(),
|
|
4623
4625
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
4626
|
+
profile_capture_nudge: exports_external.boolean().optional(),
|
|
4624
4627
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
4625
4628
|
observation_scopes: ObservationScopesSchema,
|
|
4626
4629
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -4669,7 +4672,8 @@ var init_schema = __esm(() => {
|
|
|
4669
4672
|
}).optional(),
|
|
4670
4673
|
retain: exports_external.object({
|
|
4671
4674
|
every_n_turns: exports_external.number().int().min(1).optional(),
|
|
4672
|
-
overlap_turns: exports_external.number().int().min(0).optional()
|
|
4675
|
+
overlap_turns: exports_external.number().int().min(0).optional(),
|
|
4676
|
+
tool_calls: exports_external.boolean().optional()
|
|
4673
4677
|
}).optional(),
|
|
4674
4678
|
bank_mission: exports_external.string().optional(),
|
|
4675
4679
|
reflect_mission: exports_external.string().optional(),
|
|
@@ -19645,7 +19649,7 @@ var init_resolver = __esm(() => {
|
|
|
19645
19649
|
|
|
19646
19650
|
// src/vault/broker/server.ts
|
|
19647
19651
|
import * as net from "node:net";
|
|
19648
|
-
import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as
|
|
19652
|
+
import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync8, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
19649
19653
|
|
|
19650
19654
|
// src/agents/compose.ts
|
|
19651
19655
|
init_schema();
|
|
@@ -21423,6 +21427,66 @@ function createAuditLogger(opts = {}) {
|
|
|
21423
21427
|
};
|
|
21424
21428
|
}
|
|
21425
21429
|
|
|
21430
|
+
// src/vault/broker/vault-refresh.ts
|
|
21431
|
+
init_vault();
|
|
21432
|
+
import { statSync as statSync7 } from "node:fs";
|
|
21433
|
+
function sameVaultStamp(a, b) {
|
|
21434
|
+
return a.mtimeNs === b.mtimeNs && a.size === b.size && a.ino === b.ino;
|
|
21435
|
+
}
|
|
21436
|
+
function readVaultStamp(vaultPath) {
|
|
21437
|
+
try {
|
|
21438
|
+
const st = statSync7(vaultPath, { bigint: true });
|
|
21439
|
+
return { mtimeNs: st.mtimeNs, size: st.size, ino: st.ino };
|
|
21440
|
+
} catch {
|
|
21441
|
+
return null;
|
|
21442
|
+
}
|
|
21443
|
+
}
|
|
21444
|
+
function zeroSecrets(secrets) {
|
|
21445
|
+
for (const [, entry] of Object.entries(secrets)) {
|
|
21446
|
+
try {
|
|
21447
|
+
if (entry.kind === "string" || entry.kind === "binary") {
|
|
21448
|
+
entry.value = "";
|
|
21449
|
+
}
|
|
21450
|
+
} catch {}
|
|
21451
|
+
}
|
|
21452
|
+
}
|
|
21453
|
+
function refreshVaultIfChanged(state, opts) {
|
|
21454
|
+
const { vaultPath, force = false } = opts;
|
|
21455
|
+
const open = opts.open ?? openVault;
|
|
21456
|
+
const warn = opts.warn ?? ((m) => {
|
|
21457
|
+
process.stderr.write(m);
|
|
21458
|
+
});
|
|
21459
|
+
const secrets = state.secrets;
|
|
21460
|
+
const passphrase = state.passphrase;
|
|
21461
|
+
if (secrets === null || passphrase === null)
|
|
21462
|
+
return state;
|
|
21463
|
+
const known = state.loadedStamp;
|
|
21464
|
+
if (known === null)
|
|
21465
|
+
return state;
|
|
21466
|
+
const current = readVaultStamp(vaultPath);
|
|
21467
|
+
if (current === null)
|
|
21468
|
+
return state;
|
|
21469
|
+
if (!force && sameVaultStamp(current, known))
|
|
21470
|
+
return state;
|
|
21471
|
+
if (!force && state.failedStamp !== null && sameVaultStamp(state.failedStamp, current)) {
|
|
21472
|
+
return state;
|
|
21473
|
+
}
|
|
21474
|
+
opts.beforeOpen?.(vaultPath);
|
|
21475
|
+
let next;
|
|
21476
|
+
try {
|
|
21477
|
+
next = open(passphrase, vaultPath);
|
|
21478
|
+
} catch (err) {
|
|
21479
|
+
const alreadyWarned = state.failedStamp !== null && sameVaultStamp(state.failedStamp, current);
|
|
21480
|
+
if (!alreadyWarned) {
|
|
21481
|
+
warn(`[vault-broker] WARNING: ${vaultPath} changed on disk but ` + `could not be re-opened (${err?.message ?? "unknown error"}) — ` + `continuing to serve the previously loaded secrets; will retry when ` + `the file changes again
|
|
21482
|
+
`);
|
|
21483
|
+
}
|
|
21484
|
+
return { ...state, failedStamp: current };
|
|
21485
|
+
}
|
|
21486
|
+
zeroSecrets(secrets);
|
|
21487
|
+
return { secrets: next, passphrase, loadedStamp: current, failedStamp: null };
|
|
21488
|
+
}
|
|
21489
|
+
|
|
21426
21490
|
// src/vault/grants.ts
|
|
21427
21491
|
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
21428
21492
|
|
|
@@ -23829,6 +23893,8 @@ class VaultBroker {
|
|
|
23829
23893
|
testOpts;
|
|
23830
23894
|
secrets = null;
|
|
23831
23895
|
passphrase = null;
|
|
23896
|
+
vaultStamp = null;
|
|
23897
|
+
failedVaultStamp = null;
|
|
23832
23898
|
config = null;
|
|
23833
23899
|
startedAt = Date.now();
|
|
23834
23900
|
server = null;
|
|
@@ -23923,13 +23989,53 @@ class VaultBroker {
|
|
|
23923
23989
|
}
|
|
23924
23990
|
unlockFromPassphrase(passphrase) {
|
|
23925
23991
|
detectVaultLayoutDrift(this.vaultPath);
|
|
23992
|
+
const stamp = this._readVaultStamp();
|
|
23926
23993
|
const secrets = openVault(passphrase, this.vaultPath);
|
|
23927
23994
|
this.secrets = secrets;
|
|
23928
23995
|
this.passphrase = passphrase;
|
|
23996
|
+
this.vaultStamp = stamp;
|
|
23997
|
+
this.failedVaultStamp = null;
|
|
23929
23998
|
this._setReadinessSentinel(true);
|
|
23930
23999
|
}
|
|
24000
|
+
_readVaultStamp() {
|
|
24001
|
+
return readVaultStamp(this.vaultPath);
|
|
24002
|
+
}
|
|
24003
|
+
_reloadSecretsIfVaultChanged(force = false) {
|
|
24004
|
+
let next;
|
|
24005
|
+
try {
|
|
24006
|
+
next = refreshVaultIfChanged({
|
|
24007
|
+
secrets: this.secrets,
|
|
24008
|
+
passphrase: this.passphrase,
|
|
24009
|
+
loadedStamp: this.vaultStamp,
|
|
24010
|
+
failedStamp: this.failedVaultStamp
|
|
24011
|
+
}, { vaultPath: this.vaultPath, force, beforeOpen: detectVaultLayoutDrift });
|
|
24012
|
+
} catch (err) {
|
|
24013
|
+
this._failClosedOnVaultDrift(err);
|
|
24014
|
+
return;
|
|
24015
|
+
}
|
|
24016
|
+
this.secrets = next.secrets;
|
|
24017
|
+
this.vaultStamp = next.loadedStamp;
|
|
24018
|
+
this.failedVaultStamp = next.failedStamp;
|
|
24019
|
+
}
|
|
24020
|
+
_failClosedOnVaultDrift(err) {
|
|
24021
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
24022
|
+
process.stderr.write(`[vault-broker] FATAL: ${detail}
|
|
24023
|
+
[vault-broker] Refusing to serve secrets from a vault whose on-disk ` + `layout has diverged — locking. Fix the layout and restart the broker ` + `(\`switchroom apply\`); see docs/operators/state-e-recovery.md.
|
|
24024
|
+
`);
|
|
24025
|
+
try {
|
|
24026
|
+
this.auditLogger.write({
|
|
24027
|
+
ts: new Date().toISOString(),
|
|
24028
|
+
op: "lock",
|
|
24029
|
+
caller: `pid:${process.pid}`,
|
|
24030
|
+
pid: process.pid,
|
|
24031
|
+
result: "error:vault-layout-drift"
|
|
24032
|
+
});
|
|
24033
|
+
} catch {}
|
|
24034
|
+
this.lock();
|
|
24035
|
+
}
|
|
23931
24036
|
reload(config) {
|
|
23932
24037
|
this.config = config;
|
|
24038
|
+
this._reloadSecretsIfVaultChanged(true);
|
|
23933
24039
|
}
|
|
23934
24040
|
_setReadinessSentinel(ready) {
|
|
23935
24041
|
const p = process.env.SWITCHROOM_VAULT_BROKER_READY_PATH;
|
|
@@ -23945,15 +24051,11 @@ class VaultBroker {
|
|
|
23945
24051
|
}
|
|
23946
24052
|
lock() {
|
|
23947
24053
|
if (this.secrets !== null) {
|
|
23948
|
-
|
|
23949
|
-
try {
|
|
23950
|
-
if (entry.kind === "string" || entry.kind === "binary") {
|
|
23951
|
-
entry.value = "";
|
|
23952
|
-
}
|
|
23953
|
-
} catch {}
|
|
23954
|
-
}
|
|
24054
|
+
zeroSecrets(this.secrets);
|
|
23955
24055
|
this.secrets = null;
|
|
23956
24056
|
}
|
|
24057
|
+
this.vaultStamp = null;
|
|
24058
|
+
this.failedVaultStamp = null;
|
|
23957
24059
|
this.passphrase = null;
|
|
23958
24060
|
this._setReadinessSentinel(false);
|
|
23959
24061
|
}
|
|
@@ -24273,6 +24375,7 @@ class VaultBroker {
|
|
|
24273
24375
|
socket.write(encodeResponse(errorResponse("DENIED", "preflight_access is operator-only")));
|
|
24274
24376
|
return;
|
|
24275
24377
|
}
|
|
24378
|
+
this._reloadSecretsIfVaultChanged();
|
|
24276
24379
|
if (this.secrets === null) {
|
|
24277
24380
|
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
|
24278
24381
|
return;
|
|
@@ -24304,6 +24407,7 @@ class VaultBroker {
|
|
|
24304
24407
|
return;
|
|
24305
24408
|
}
|
|
24306
24409
|
if (req.op === "list") {
|
|
24410
|
+
this._reloadSecretsIfVaultChanged();
|
|
24307
24411
|
if (this.secrets === null) {
|
|
24308
24412
|
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
|
24309
24413
|
return;
|
|
@@ -24379,6 +24483,7 @@ class VaultBroker {
|
|
|
24379
24483
|
return;
|
|
24380
24484
|
}
|
|
24381
24485
|
if (req.op === "get") {
|
|
24486
|
+
this._reloadSecretsIfVaultChanged();
|
|
24382
24487
|
if (this.secrets === null) {
|
|
24383
24488
|
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
|
24384
24489
|
return;
|
|
@@ -24566,10 +24671,24 @@ class VaultBroker {
|
|
|
24566
24671
|
return;
|
|
24567
24672
|
}
|
|
24568
24673
|
if (req.op === "put") {
|
|
24674
|
+
this._reloadSecretsIfVaultChanged();
|
|
24569
24675
|
if (this.secrets === null || this.passphrase === null) {
|
|
24570
24676
|
socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
|
|
24571
24677
|
return;
|
|
24572
24678
|
}
|
|
24679
|
+
if (this.failedVaultStamp !== null) {
|
|
24680
|
+
writeAudit({
|
|
24681
|
+
ts: new Date().toISOString(),
|
|
24682
|
+
op: "put",
|
|
24683
|
+
key: req.key,
|
|
24684
|
+
caller: auditCaller,
|
|
24685
|
+
pid: auditPid,
|
|
24686
|
+
cgroup: auditCgroup,
|
|
24687
|
+
result: "denied:vault-file-unreadable"
|
|
24688
|
+
});
|
|
24689
|
+
socket.write(encodeResponse(errorResponse("INTERNAL", "put refused: the vault file on disk changed and could not be decrypted with the passphrase this broker holds. Writing now would overwrite it with the broker's older in-memory state. Restore the vault file the broker was unlocked with, or restart the broker and unlock it with the current passphrase (reads keep serving the previously loaded secrets meanwhile).")));
|
|
24690
|
+
return;
|
|
24691
|
+
}
|
|
24573
24692
|
let passphraseAttested = false;
|
|
24574
24693
|
const requestedPostureAttest = req.attest_via_posture === true;
|
|
24575
24694
|
if (requestedPostureAttest && req.passphrase !== undefined && req.passphrase !== "") {
|
|
@@ -24813,6 +24932,8 @@ class VaultBroker {
|
|
|
24813
24932
|
return;
|
|
24814
24933
|
}
|
|
24815
24934
|
this.chownVaultToOperator();
|
|
24935
|
+
this.vaultStamp = this._readVaultStamp();
|
|
24936
|
+
this.failedVaultStamp = null;
|
|
24816
24937
|
this.auditLogger.write({
|
|
24817
24938
|
ts: new Date().toISOString(),
|
|
24818
24939
|
op: "put",
|
|
@@ -25388,7 +25509,7 @@ class VaultBroker {
|
|
|
25388
25509
|
if (!existsSync13(filePath))
|
|
25389
25510
|
return false;
|
|
25390
25511
|
try {
|
|
25391
|
-
if (
|
|
25512
|
+
if (statSync8(filePath).size === 0)
|
|
25392
25513
|
return false;
|
|
25393
25514
|
} catch {
|
|
25394
25515
|
return false;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.15",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -1189,6 +1189,15 @@ export HINDSIGHT_RECALL_SKIP_TRIVIAL={{hindsightRecallSkipTrivial}}
|
|
|
1189
1189
|
{{#if hindsightDirectiveCaptureNudge}}
|
|
1190
1190
|
export HINDSIGHT_DIRECTIVE_CAPTURE_NUDGE={{hindsightDirectiveCaptureNudge}}
|
|
1191
1191
|
{{/if}}
|
|
1192
|
+
# RFC phase4 P3 — operator-profile capture nudge (memory.profile_capture_nudge
|
|
1193
|
+
# cascade). On by default (plugin settings.json): recall.py regex-detects a
|
|
1194
|
+
# first-person durable self-statement by the operator and nudges the model to
|
|
1195
|
+
# persist it with an explicit retain tagged `profile:ken` into the agent's own
|
|
1196
|
+
# bank. Export only when the operator overrode it; set false to disable the
|
|
1197
|
+
# nudge for this agent.
|
|
1198
|
+
{{#if hindsightProfileCaptureNudge}}
|
|
1199
|
+
export HINDSIGHT_PROFILE_CAPTURE_NUDGE={{hindsightProfileCaptureNudge}}
|
|
1200
|
+
{{/if}}
|
|
1192
1201
|
# Per-row observation scope (memory.observation_scopes cascade). Stamped on
|
|
1193
1202
|
# every retain — Stop hook, sidechain, boot reconcile, queue drain, backfill.
|
|
1194
1203
|
# "shared" makes consolidation pool this agent's observations into ONE global
|
|
@@ -1,92 +1,38 @@
|
|
|
1
1
|
## Self-service: scheduled tasks (cron) and config introspection
|
|
2
2
|
|
|
3
|
-
You have an **`agent-config`** MCP server
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
tools
|
|
3
|
+
You have an **`agent-config`** MCP server for inspecting your own config and
|
|
4
|
+
creating/removing your own scheduled tasks — don't hand the user a yaml
|
|
5
|
+
snippet, do the edit yourself.
|
|
6
|
+
|
|
7
|
+
- Write tools: `schedule_add` (e.g. `cron_expr: "0 17 * * 0"` for "call mom
|
|
8
|
+
every Sunday at 5pm", or `"0 8 * * 1-5"` for a weekday-morning digest) /
|
|
9
|
+
`schedule_remove`; `skill_install` (`source: "bundled:<name>"`) /
|
|
10
|
+
`skill_remove`; and the personal-skill set `skill_init_personal` /
|
|
11
|
+
`skill_clone_to_personal` / `skill_edit_personal` / `skill_remove_personal`.
|
|
12
|
+
If YOU find a bug in a skill you're using, fork-and-fix is your OWN call,
|
|
13
|
+
not the operator's: `skill_clone_to_personal` then `skill_edit_personal`,
|
|
14
|
+
then use `personal-<name>`.
|
|
15
|
+
- Read tools: `cron_list`, `cron_doctor`, `skill_list`, `skill_list_personal`,
|
|
16
|
+
`skill_search` (answers "is there a skill for X?"), `config_get`, `whoami`,
|
|
17
|
+
`audit_tail`, `peers_list` (live — never cache or memorize the fleet into
|
|
18
|
+
memory).
|
|
19
|
+
- Safety rails the broker hard-rejects, so know them before promising
|
|
20
|
+
anything: **5-minute minimum interval** (offer `*/5`, `*/15`, or `*/30`
|
|
21
|
+
instead of anything faster) and **20 entries** each for schedules and
|
|
22
|
+
skills — check `cron_list` / `skill_list` first if you're near a cap.
|
|
23
|
+
These numbers live only in the broker's rejection messages, not the tool
|
|
24
|
+
schema — know them before the call, not after the error. No `secrets:` on
|
|
25
|
+
agent-authored cron entries — go through `vault_request_access` at
|
|
26
|
+
runtime instead. You manage only your OWN schedule/skills — cross-agent
|
|
27
|
+
writes are refused; for another agent, point the user to it via
|
|
28
|
+
`peers_list`.
|
|
29
|
+
- After a successful `schedule_add`, tell the user the human-readable
|
|
30
|
+
schedule, the cron expression you actually wrote, and the `name` slug (so
|
|
31
|
+
they can remove it later). After a failed write, surface the error
|
|
32
|
+
verbatim and the closest legal alternative.
|
|
33
|
+
- Cron-fired sessions render rich markdown, so scheduled/digest output should
|
|
34
|
+
use **bold** headers and `code` for identifiers, just like a live reply.
|
|
8
35
|
|
|
9
|
-
### When to reach for these tools
|
|
10
|
-
|
|
11
|
-
| User says… | You call… |
|
|
12
|
-
|---|---|
|
|
13
|
-
| "remind me to call mom every Sunday at 5pm" | `schedule_add` with `cron_expr: "0 17 * * 0"` |
|
|
14
|
-
| "run the morning digest at 8am every weekday" | `schedule_add` with `cron_expr: "0 8 * * 1-5"` |
|
|
15
|
-
| "check the build every 15 minutes" | `schedule_add` with `cron_expr: "*/15 * * * *"` (legal — 15min ≥ the 5-min floor) |
|
|
16
|
-
| "ping me every 2 minutes" | rejected by the 5-min floor — offer `*/5` or `*/10` instead |
|
|
17
|
-
| "stop the daily digest" | `cron_list` to find the entry, then `schedule_remove` by `name` |
|
|
18
|
-
| "what tasks am I running on a schedule?" | `cron_list` |
|
|
19
|
-
| "what skills do I have access to?" | `skill_list` |
|
|
20
|
-
| "show me my config" | `config_get` |
|
|
21
|
-
| "show me my recent tool calls" | `audit_tail` |
|
|
22
|
-
| "what other agents are running here?" / "is there an agent that does X?" / "who handles Y?" | `peers_list` |
|
|
23
|
-
| "install the foo skill" / "give yourself the foo skill" | `skill_install` with `source: "bundled:foo"` |
|
|
24
|
-
| "drop the foo skill" / "remove the foo skill" | `skill_remove` with `name: "foo"` |
|
|
25
|
-
| "is there a skill for X?" | `skill_search` with `query: "X"` |
|
|
26
|
-
| **YOU find a bug in a skill you're using** | `skill_clone_to_personal` then `skill_edit_personal` — fork-and-fix yourself |
|
|
27
|
-
| "write me a custom skill that does X" | `skill_init_personal` |
|
|
28
|
-
|
|
29
|
-
The tool schemas carry the parameters, enums, and error codes — read them
|
|
30
|
-
there. What follows is only what you need *before* the call, because it
|
|
31
|
-
changes the advice you give the user.
|
|
32
|
-
|
|
33
|
-
### Writing a cron prompt
|
|
34
|
-
|
|
35
|
-
`schedule_add`'s `prompt` is what *you* receive when the cron fires — phrase it
|
|
36
|
-
from your future-self's perspective (`"Time for the daily digest — pull
|
|
37
|
-
yesterday's GitHub activity and DM the summary to chat 12345"`, not `"please
|
|
38
|
-
send the digest"`). A new entry takes effect within **~30s**; the scheduler
|
|
39
|
-
hot-reloads, no restart needed.
|
|
40
|
-
|
|
41
|
-
### Mind the cost — pick the cheapest tier that does the job
|
|
42
|
-
|
|
43
|
-
- **Tier 1 — cheap fresh session.** The default for a **frequent** cron (≤60min):
|
|
44
|
-
a fresh Sonnet that keeps your memory + tools but drops your conversation
|
|
45
|
-
context. Force it for a daily/weekly self-contained job with `model:
|
|
46
|
-
"sonnet"` / `context: "fresh"`.
|
|
47
|
-
- **Tier 2 — a full turn in your live session.** The default for **daily/weekly**
|
|
48
|
-
crons. `context: "agent"` pins a fire here when it genuinely needs your
|
|
49
|
-
accumulated context (this always wins).
|
|
50
|
-
- **Model-free tiers — you must REQUEST these from the operator** (they need a
|
|
51
|
-
config commit; you can only self-author plain prompt crons plus the
|
|
52
|
-
`model`/`context` hints): **`kind: action`** posts a FIXED text at zero
|
|
53
|
-
tokens (renders full GFM — set `parse_mode: text` for verbatim);
|
|
54
|
-
**`kind: poll`** is a model-free check where only a *change* wakes you —
|
|
55
|
-
don't burn a frequent prompt cron on polling; **`reaction_dispatch`** wakes
|
|
56
|
-
you instantly on an emoji reaction, zero polling.
|
|
57
|
-
|
|
58
|
-
Cron-fired sessions render rich markdown, so scheduled/digest output should use
|
|
59
|
-
**bold** headers and `code` for identifiers, just like a live reply.
|
|
60
|
-
|
|
61
|
-
### Safety rails — know these BEFORE you promise anything
|
|
62
|
-
|
|
63
|
-
The broker hard-rejects writes that violate these, so they shape the advice
|
|
64
|
-
you give — don't surprise the user with an error after agreeing to something
|
|
65
|
-
the rails will block:
|
|
66
|
-
|
|
67
|
-
- **Minimum 5-minute interval.** Anything more frequent (`* * * * *`, `*/2`,
|
|
68
|
-
`*/3`, `*/4`) is rejected. Offer `*/5`, `*/15`, `*/30`, or hourly instead.
|
|
69
|
-
- **20 schedule entries per agent.** Near the cap, `cron_list` first; if full,
|
|
70
|
-
ask the user which one to drop before adding.
|
|
71
|
-
- **20 skills per agent**, same shape.
|
|
72
|
-
- **No `secrets:` on agent-authored entries.** Cron fires the prompt; you go
|
|
73
|
-
through `vault_request_access` at runtime instead.
|
|
74
|
-
- **You manage only your OWN schedule and skills.** The broker pins identity
|
|
75
|
-
via `$SWITCHROOM_AGENT_NAME`; for another agent, tell the user to ask that
|
|
76
|
-
agent. `skill_remove` likewise won't remove skills the operator wrote
|
|
77
|
-
directly into `switchroom.yaml` — those are operator-only.
|
|
78
|
-
- **`peers_list` is live** — never cache or memorize the fleet into memory.
|
|
79
|
-
|
|
80
|
-
Fixing a skill you're using is your call, not the operator's:
|
|
81
|
-
`skill_clone_to_personal` → `skill_edit_personal` → use `personal-<name>`. The
|
|
82
|
-
fork is durable and auto-mirrors to `~/.switchroom-config/` when present.
|
|
83
|
-
|
|
84
|
-
### Confirm honestly
|
|
85
|
-
|
|
86
|
-
After a successful `schedule_add`, tell the user the human-readable schedule,
|
|
87
|
-
the cron expression you actually wrote, and the `name` slug (so they can
|
|
88
|
-
remove it later). After a failed write, surface the error verbatim, name
|
|
89
|
-
which rail tripped, and offer the closest legal alternative.
|
|
90
36
|
{{#if linearAgentEnabled}}
|
|
91
37
|
|
|
92
38
|
## Capture to Linear (👨💻 / 📌 reaction → new issue)
|
|
@@ -2,65 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Vault & secrets
|
|
4
4
|
|
|
5
|
-
Secrets
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
the container.
|
|
23
|
-
|
|
24
|
-
### When you hit `VAULT-BROKER-DENIED`
|
|
25
|
-
|
|
26
|
-
You don't have a grant for that key yet. Recovery depends on whether
|
|
27
|
-
there's an operator in the loop:
|
|
28
|
-
|
|
29
|
-
- **Interactive context** (you're handling an inbound chat message,
|
|
30
|
-
i.e. you have a `chat_id` available): call the
|
|
31
|
-
`vault_request_access` MCP tool with `key='<key>'`, `scope='read'`,
|
|
32
|
-
and a one-line `reason`. This renders a `[✅ Approve] [🚫 Deny]`
|
|
33
|
-
card in the chat. After firing the tool, **end your turn cleanly** —
|
|
34
|
-
the gateway will inject a fresh inbound (`<channel source="vault_grant_approved">`)
|
|
35
|
-
when the operator approves, kicking off a new turn where you can
|
|
36
|
-
resume the task.
|
|
37
|
-
|
|
38
|
-
- **Non-interactive context** (cron fire, `meta.source="cron"`, no
|
|
39
|
-
operator chat): do **not** spam approval cards into an empty topic.
|
|
40
|
-
Log the missing capability in your output, degrade gracefully
|
|
41
|
-
(clearly-marked estimates / a "skipped — needs vault grant" status
|
|
42
|
-
/ etc.), and continue. The operator will see the gap in your next
|
|
43
|
-
interactive turn and can grant access then.
|
|
44
|
-
|
|
45
|
-
### What never works from inside the agent
|
|
46
|
-
|
|
47
|
-
- `switchroom vault get --no-broker <key>` — the vault file isn't
|
|
48
|
-
mounted; this exits with `VAULT-SANDBOX-CONTEXT`. The flag exists
|
|
49
|
-
for the operator on the host, not for you.
|
|
50
|
-
- Reading a credentials env file from disk (e.g.
|
|
51
|
-
`~/.switchroom/credentials/<service>.env`). If you see code that
|
|
52
|
-
does this, treat it as a bug to fix, not a fallback to rely on —
|
|
53
|
-
unencrypted secrets on disk defeat the whole vault model.
|
|
54
|
-
- Asking the operator to paste the secret into Telegram. The secret-
|
|
55
|
-
scrub hooks will redact it, and you've leaked it to chat history
|
|
56
|
-
along the way. Always use the `vault_request_access` flow.
|
|
57
|
-
|
|
58
|
-
### When a sub-agent or tool reports a service/API/credential as "inaccessible"
|
|
59
|
-
|
|
60
|
-
That's the broker wall surfacing as prose — treat it as `VAULT-BROKER-DENIED`. Don't ask the operator to paste credentials or open a dashboard: infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`), call `vault_request_access` with it, `scope='read'`, and a one-line reason, then end your turn cleanly and resume on the grant. Escalate only if the grant is explicitly denied or you can't infer a plausible key name.
|
|
61
|
-
|
|
62
|
-
### Hint: the deny stderr tells you the exact recovery
|
|
63
|
-
|
|
64
|
-
The CLI emits a marker + actionable hint on every vault failure. Read
|
|
65
|
-
the **second line** — it names the right tool for your situation,
|
|
66
|
-
sandbox-aware. Trust it instead of guessing.
|
|
5
|
+
Secrets live in the encrypted vault, read only via the vault-broker — never
|
|
6
|
+
direct file IO (e.g. `~/.switchroom/credentials/*.env` — if code does this,
|
|
7
|
+
it's a bug, not a fallback), never asking the operator to paste one into
|
|
8
|
+
chat (the scrub hooks redact it anyway, but it's already leaked to history).
|
|
9
|
+
Reference as `vault:<key>` in config; `switchroom vault get <key>` in a
|
|
10
|
+
shell script. Never add `--no-broker` — the vault file isn't mounted in
|
|
11
|
+
here, so it always fails; that flag is host-only, for the operator.
|
|
12
|
+
|
|
13
|
+
On `VAULT-BROKER-DENIED` (or a tool/sub-agent reporting a service as
|
|
14
|
+
"inaccessible" — same wall, infer the likely `<service>/<key>`): if you have
|
|
15
|
+
a `chat_id`, call `vault_request_access` and end your turn cleanly — a fresh
|
|
16
|
+
inbound wakes you on approval. In a non-interactive context (cron, no
|
|
17
|
+
operator chat), don't spam a card into an empty topic — degrade gracefully
|
|
18
|
+
and note the gap for your next interactive turn.
|
|
19
|
+
|
|
20
|
+
Every vault CLI failure's stderr line 2 names the exact recovery for your
|
|
21
|
+
situation — trust it over guessing.
|