switchroom 0.21.16 → 0.21.18
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 +5 -0
- package/dist/auth-broker/index.js +5 -0
- package/dist/cli/notion-write-pretool.mjs +5 -0
- package/dist/cli/switchroom.js +1492 -1136
- package/dist/host-control/main.js +6 -1
- package/dist/vault/approvals/kernel-server.js +5 -0
- package/dist/vault/broker/server.js +5 -0
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +11 -0
- package/telegram-plugin/dist/gateway/gateway.js +9 -4
- package/telegram-plugin/scripts/bun-test-ci.sh +6 -0
- package/telegram-plugin/uat/flip/allowlist.test.ts +229 -0
- package/telegram-plugin/uat/flip/allowlist.ts +349 -0
- package/telegram-plugin/uat/flip/gate.test.ts +153 -0
- package/telegram-plugin/uat/flip/gate.ts +232 -0
- package/telegram-plugin/uat/flip/probe-scoring.test.ts +210 -0
- package/telegram-plugin/uat/flip/probe-scoring.ts +200 -0
- package/telegram-plugin/uat/flip/probe-suite.test.ts +95 -0
- package/telegram-plugin/uat/flip/probe-suite.ts +155 -0
- package/telegram-plugin/uat/flip/probes/kdogg.probes.json +36 -0
- package/telegram-plugin/uat/flip/probes/test-harness.probes.json +15 -0
- package/telegram-plugin/uat/flip/recall-log.test.ts +131 -0
- package/telegram-plugin/uat/flip/recall-log.ts +178 -0
- package/telegram-plugin/uat/flip/report.ts +95 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.test.ts +470 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.ts +697 -0
- package/telegram-plugin/uat/flip/tier2-probe-runner.ts +327 -0
- package/telegram-plugin/uat/runners/scorer.ts +1 -1
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +43 -1
- package/vendor/hindsight-memory/scripts/lib/client.py +35 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +47 -0
- package/vendor/hindsight-memory/scripts/lib/orientation.py +248 -0
- package/vendor/hindsight-memory/scripts/lib/recall_buffer.py +29 -0
- package/vendor/hindsight-memory/scripts/orientation.py +195 -0
- package/vendor/hindsight-memory/scripts/prefetch.py +10 -0
- package/vendor/hindsight-memory/scripts/recall.py +144 -11
- package/vendor/hindsight-memory/scripts/setup_hooks.py +10 -1
- package/vendor/hindsight-memory/scripts/tests/fixtures/rules-block.golden.md +9 -0
- package/vendor/hindsight-memory/scripts/tests/test_orientation_hook.py +283 -0
- package/vendor/hindsight-memory/scripts/tests/test_orientation_logic.py +176 -0
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_invalidation.py +329 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_directive_suppression.py +328 -0
|
@@ -11144,6 +11144,10 @@ var init_schema = __esm(() => {
|
|
|
11144
11144
|
}).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry — e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced — 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded — each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
|
|
11145
11145
|
observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
|
|
11146
11146
|
rules_block: exports_external.boolean().default(false).describe("Memory v2 M3 go-live flag (M1 only DEFINES this, default false; " + "M3 flips it per agent). When true, the sanctioned rules/index " + "blocks render live in this agent's CLAUDE.md (marker-delimited, " + "below the `# --- Yours ---` line) AND the permission deny for " + "direct Edit/Write of the agent's own CLAUDE.md is seeded — the " + "flag couples deny + tools together so a live deny never orphans " + "the invited free-text edit path (the `memory_edit_yours` verb is " + "the only sanctioned writer once flipped). Unset/false ⇒ byte-" + "identical to pre-M1 behaviour: no blocks, no deny, dark build. " + "Cascade: override (per-agent wins over default; never fleet-" + "seeded — each agent's flip is a deliberate M3 rollout step)."),
|
|
11147
|
+
orientation: exports_external.boolean().optional().describe("Memory v2 M5 go-live flag (Surface B: orientation-at-boot), " + "effective default false — dark build (unset ⇒ OFF, applied by the " + "scaffold resolver, NOT a Zod .default() — a hard default here would " + "shadow the per-agent value in the cascade the way it does every other " + "memory knob). When true, the orientation SessionStart " + "hook injects this agent's cron-refreshed `orientation` mental model " + "into context at boot AND re-seats it after every compaction (E-88), " + "deterministically and with zero tool call. Unset/false ⇒ byte-" + "identical to pre-M5 behaviour: the hook no-ops before any bank " + "resolve or network call. Per-agent ONLY (never fleet-seeded — a " + "flip is a deliberate canary/rollout step gated on the agent's " + "m2-residue measurement + operator-created model, carve-M5 §7). " + "Cascade: override (per-agent wins over default)."),
|
|
11148
|
+
orientation_reinject_turns: exports_external.number().int().min(0).optional().describe("Memory v2 M5 optional per-turn re-inject cadence, effective default " + "0 = off (unset ⇒ 0, applied by the scaffold resolver, not a Zod " + ".default() — same cascade-shadowing reason as `orientation`). At 0 the " + "briefing is injected only at SessionStart + compaction (the cheap " + "path). N>0 " + "would additionally re-inject the orientation briefing every N turns; " + "priced at ~55M tok/30d at every-turn, which is WHY it defaults off " + "(carve-M5 §2/§4). Only late-session context loss should ever set it, " + "per-agent, measured. A stripped/absent value fails to 0 (fail-safe " + "AND fail-cheap). Per-agent ONLY. Cascade: override."),
|
|
11149
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional().describe("Memory v2 M5 per-agent refresh cadence tier (hours), effective " + "default 48 (unset ⇒ 48, applied by the scaffold resolver, not a Zod " + ".default() — a hard default would shadow a `defaults.memory." + "orientation_cadence_hours` fleet value in the cascade). " + "The fast-churning agents (klanker, overlord) set 24; everyone else " + "inherits 48 (carve-M5 §6, Ken's locked tiered cadence). Drives BOTH " + "the out-of-session refresh scheduler AND the staleness guard's " + "per-tier thresholds (stale prefix at 1.5× cadence, cold degrade at " + "3×) — so a fixed 36h can never mislabel a 48h-tier agent as stale a " + "day early. Modelled as an explicit int (not a hard-coded name list) " + "so the tiering is data-driven and auditable. A cost knob, not an " + "enablement knob: a stripped/absent value failing to 48 is safe " + "(the cheaper tier). Cascade: override (per-agent wins over default); " + "unlike `orientation`/`_reinject_turns` it IS accepted at the " + "defaults/profile tier."),
|
|
11150
|
+
inject_directives: exports_external.boolean().optional().describe("Memory v2 M3 Surface-A directive-injection switch (default TRUE — " + "on-by-default via the plugin settings.json stamp + config.py " + "DEFAULTS). When left unset the agent keeps injecting the " + "`<active_directives>` block on every turn, byte-identical to pre-M3. " + "Set FALSE to SUPPRESS that injection for this agent — its standing " + "rules are then served from the live rules block (memory.rules_block) " + "instead of re-injected directives, which is the change that collapses " + "the always-on directive-token spend (E-41). The suppression is " + "fail-safe and re-checked every turn in recall.py: it fires ONLY when " + "a non-empty rules block is physically present in the agent's " + "CLAUDE.md — a false flag with an empty/absent block keeps injecting " + "AND emits a degraded-canary notice, so a zero-standing-rules turn is " + "unreachable. Requires memory.rules_block=true first (the block must " + "be live before injection is turned off — enforce the ordering with " + "`switchroom memory flip`, never a bare flag flip). Coordinates with " + "M4's memory.recall prefetch (memoryPrefetchEnabled): the same guard " + "gates all three recall.py emit sites so a flipped agent leaks no " + "directive on a cache/prefetch hit. Cascade: override, per-agent ONLY " + "(never fleet-seeded from the defaults/profile tier — each flip is a " + "deliberate staggered M3 rollout step, same darkness safeguard as " + "rules_block). Default-true is the fail-safe direction under the " + "Zod-strip footgun: a stripped/misread key keeps the agent injecting."),
|
|
11147
11151
|
disposition: exports_external.object({
|
|
11148
11152
|
skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
|
|
11149
11153
|
literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
|
|
@@ -11589,6 +11593,7 @@ var init_schema = __esm(() => {
|
|
|
11589
11593
|
profile: exports_external.string().optional(),
|
|
11590
11594
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
11591
11595
|
profile_capture_nudge: exports_external.boolean().optional(),
|
|
11596
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional(),
|
|
11592
11597
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
11593
11598
|
observation_scopes: ObservationScopesSchema,
|
|
11594
11599
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -21593,7 +21598,7 @@ function allocateAgentUid(name) {
|
|
|
21593
21598
|
}
|
|
21594
21599
|
|
|
21595
21600
|
// src/build-info.ts
|
|
21596
|
-
var VERSION = "0.21.
|
|
21601
|
+
var VERSION = "0.21.18";
|
|
21597
21602
|
|
|
21598
21603
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21599
21604
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
@@ -4584,6 +4584,10 @@ var init_schema = __esm(() => {
|
|
|
4584
4584
|
}).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry — e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced — 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded — each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
|
|
4585
4585
|
observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
|
|
4586
4586
|
rules_block: exports_external.boolean().default(false).describe("Memory v2 M3 go-live flag (M1 only DEFINES this, default false; " + "M3 flips it per agent). When true, the sanctioned rules/index " + "blocks render live in this agent's CLAUDE.md (marker-delimited, " + "below the `# --- Yours ---` line) AND the permission deny for " + "direct Edit/Write of the agent's own CLAUDE.md is seeded — the " + "flag couples deny + tools together so a live deny never orphans " + "the invited free-text edit path (the `memory_edit_yours` verb is " + "the only sanctioned writer once flipped). Unset/false ⇒ byte-" + "identical to pre-M1 behaviour: no blocks, no deny, dark build. " + "Cascade: override (per-agent wins over default; never fleet-" + "seeded — each agent's flip is a deliberate M3 rollout step)."),
|
|
4587
|
+
orientation: exports_external.boolean().optional().describe("Memory v2 M5 go-live flag (Surface B: orientation-at-boot), " + "effective default false — dark build (unset ⇒ OFF, applied by the " + "scaffold resolver, NOT a Zod .default() — a hard default here would " + "shadow the per-agent value in the cascade the way it does every other " + "memory knob). When true, the orientation SessionStart " + "hook injects this agent's cron-refreshed `orientation` mental model " + "into context at boot AND re-seats it after every compaction (E-88), " + "deterministically and with zero tool call. Unset/false ⇒ byte-" + "identical to pre-M5 behaviour: the hook no-ops before any bank " + "resolve or network call. Per-agent ONLY (never fleet-seeded — a " + "flip is a deliberate canary/rollout step gated on the agent's " + "m2-residue measurement + operator-created model, carve-M5 §7). " + "Cascade: override (per-agent wins over default)."),
|
|
4588
|
+
orientation_reinject_turns: exports_external.number().int().min(0).optional().describe("Memory v2 M5 optional per-turn re-inject cadence, effective default " + "0 = off (unset ⇒ 0, applied by the scaffold resolver, not a Zod " + ".default() — same cascade-shadowing reason as `orientation`). At 0 the " + "briefing is injected only at SessionStart + compaction (the cheap " + "path). N>0 " + "would additionally re-inject the orientation briefing every N turns; " + "priced at ~55M tok/30d at every-turn, which is WHY it defaults off " + "(carve-M5 §2/§4). Only late-session context loss should ever set it, " + "per-agent, measured. A stripped/absent value fails to 0 (fail-safe " + "AND fail-cheap). Per-agent ONLY. Cascade: override."),
|
|
4589
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional().describe("Memory v2 M5 per-agent refresh cadence tier (hours), effective " + "default 48 (unset ⇒ 48, applied by the scaffold resolver, not a Zod " + ".default() — a hard default would shadow a `defaults.memory." + "orientation_cadence_hours` fleet value in the cascade). " + "The fast-churning agents (klanker, overlord) set 24; everyone else " + "inherits 48 (carve-M5 §6, Ken's locked tiered cadence). Drives BOTH " + "the out-of-session refresh scheduler AND the staleness guard's " + "per-tier thresholds (stale prefix at 1.5× cadence, cold degrade at " + "3×) — so a fixed 36h can never mislabel a 48h-tier agent as stale a " + "day early. Modelled as an explicit int (not a hard-coded name list) " + "so the tiering is data-driven and auditable. A cost knob, not an " + "enablement knob: a stripped/absent value failing to 48 is safe " + "(the cheaper tier). Cascade: override (per-agent wins over default); " + "unlike `orientation`/`_reinject_turns` it IS accepted at the " + "defaults/profile tier."),
|
|
4590
|
+
inject_directives: exports_external.boolean().optional().describe("Memory v2 M3 Surface-A directive-injection switch (default TRUE — " + "on-by-default via the plugin settings.json stamp + config.py " + "DEFAULTS). When left unset the agent keeps injecting the " + "`<active_directives>` block on every turn, byte-identical to pre-M3. " + "Set FALSE to SUPPRESS that injection for this agent — its standing " + "rules are then served from the live rules block (memory.rules_block) " + "instead of re-injected directives, which is the change that collapses " + "the always-on directive-token spend (E-41). The suppression is " + "fail-safe and re-checked every turn in recall.py: it fires ONLY when " + "a non-empty rules block is physically present in the agent's " + "CLAUDE.md — a false flag with an empty/absent block keeps injecting " + "AND emits a degraded-canary notice, so a zero-standing-rules turn is " + "unreachable. Requires memory.rules_block=true first (the block must " + "be live before injection is turned off — enforce the ordering with " + "`switchroom memory flip`, never a bare flag flip). Coordinates with " + "M4's memory.recall prefetch (memoryPrefetchEnabled): the same guard " + "gates all three recall.py emit sites so a flipped agent leaks no " + "directive on a cache/prefetch hit. Cascade: override, per-agent ONLY " + "(never fleet-seeded from the defaults/profile tier — each flip is a " + "deliberate staggered M3 rollout step, same darkness safeguard as " + "rules_block). Default-true is the fail-safe direction under the " + "Zod-strip footgun: a stripped/misread key keeps the agent injecting."),
|
|
4587
4591
|
disposition: exports_external.object({
|
|
4588
4592
|
skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
|
|
4589
4593
|
literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
|
|
@@ -5029,6 +5033,7 @@ var init_schema = __esm(() => {
|
|
|
5029
5033
|
profile: exports_external.string().optional(),
|
|
5030
5034
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
5031
5035
|
profile_capture_nudge: exports_external.boolean().optional(),
|
|
5036
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional(),
|
|
5032
5037
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
5033
5038
|
observation_scopes: ObservationScopesSchema,
|
|
5034
5039
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -4180,6 +4180,10 @@ var init_schema = __esm(() => {
|
|
|
4180
4180
|
}).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry — e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced — 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded — each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
|
|
4181
4181
|
observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
|
|
4182
4182
|
rules_block: exports_external.boolean().default(false).describe("Memory v2 M3 go-live flag (M1 only DEFINES this, default false; " + "M3 flips it per agent). When true, the sanctioned rules/index " + "blocks render live in this agent's CLAUDE.md (marker-delimited, " + "below the `# --- Yours ---` line) AND the permission deny for " + "direct Edit/Write of the agent's own CLAUDE.md is seeded — the " + "flag couples deny + tools together so a live deny never orphans " + "the invited free-text edit path (the `memory_edit_yours` verb is " + "the only sanctioned writer once flipped). Unset/false ⇒ byte-" + "identical to pre-M1 behaviour: no blocks, no deny, dark build. " + "Cascade: override (per-agent wins over default; never fleet-" + "seeded — each agent's flip is a deliberate M3 rollout step)."),
|
|
4183
|
+
orientation: exports_external.boolean().optional().describe("Memory v2 M5 go-live flag (Surface B: orientation-at-boot), " + "effective default false — dark build (unset ⇒ OFF, applied by the " + "scaffold resolver, NOT a Zod .default() — a hard default here would " + "shadow the per-agent value in the cascade the way it does every other " + "memory knob). When true, the orientation SessionStart " + "hook injects this agent's cron-refreshed `orientation` mental model " + "into context at boot AND re-seats it after every compaction (E-88), " + "deterministically and with zero tool call. Unset/false ⇒ byte-" + "identical to pre-M5 behaviour: the hook no-ops before any bank " + "resolve or network call. Per-agent ONLY (never fleet-seeded — a " + "flip is a deliberate canary/rollout step gated on the agent's " + "m2-residue measurement + operator-created model, carve-M5 §7). " + "Cascade: override (per-agent wins over default)."),
|
|
4184
|
+
orientation_reinject_turns: exports_external.number().int().min(0).optional().describe("Memory v2 M5 optional per-turn re-inject cadence, effective default " + "0 = off (unset ⇒ 0, applied by the scaffold resolver, not a Zod " + ".default() — same cascade-shadowing reason as `orientation`). At 0 the " + "briefing is injected only at SessionStart + compaction (the cheap " + "path). N>0 " + "would additionally re-inject the orientation briefing every N turns; " + "priced at ~55M tok/30d at every-turn, which is WHY it defaults off " + "(carve-M5 §2/§4). Only late-session context loss should ever set it, " + "per-agent, measured. A stripped/absent value fails to 0 (fail-safe " + "AND fail-cheap). Per-agent ONLY. Cascade: override."),
|
|
4185
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional().describe("Memory v2 M5 per-agent refresh cadence tier (hours), effective " + "default 48 (unset ⇒ 48, applied by the scaffold resolver, not a Zod " + ".default() — a hard default would shadow a `defaults.memory." + "orientation_cadence_hours` fleet value in the cascade). " + "The fast-churning agents (klanker, overlord) set 24; everyone else " + "inherits 48 (carve-M5 §6, Ken's locked tiered cadence). Drives BOTH " + "the out-of-session refresh scheduler AND the staleness guard's " + "per-tier thresholds (stale prefix at 1.5× cadence, cold degrade at " + "3×) — so a fixed 36h can never mislabel a 48h-tier agent as stale a " + "day early. Modelled as an explicit int (not a hard-coded name list) " + "so the tiering is data-driven and auditable. A cost knob, not an " + "enablement knob: a stripped/absent value failing to 48 is safe " + "(the cheaper tier). Cascade: override (per-agent wins over default); " + "unlike `orientation`/`_reinject_turns` it IS accepted at the " + "defaults/profile tier."),
|
|
4186
|
+
inject_directives: exports_external.boolean().optional().describe("Memory v2 M3 Surface-A directive-injection switch (default TRUE — " + "on-by-default via the plugin settings.json stamp + config.py " + "DEFAULTS). When left unset the agent keeps injecting the " + "`<active_directives>` block on every turn, byte-identical to pre-M3. " + "Set FALSE to SUPPRESS that injection for this agent — its standing " + "rules are then served from the live rules block (memory.rules_block) " + "instead of re-injected directives, which is the change that collapses " + "the always-on directive-token spend (E-41). The suppression is " + "fail-safe and re-checked every turn in recall.py: it fires ONLY when " + "a non-empty rules block is physically present in the agent's " + "CLAUDE.md — a false flag with an empty/absent block keeps injecting " + "AND emits a degraded-canary notice, so a zero-standing-rules turn is " + "unreachable. Requires memory.rules_block=true first (the block must " + "be live before injection is turned off — enforce the ordering with " + "`switchroom memory flip`, never a bare flag flip). Coordinates with " + "M4's memory.recall prefetch (memoryPrefetchEnabled): the same guard " + "gates all three recall.py emit sites so a flipped agent leaks no " + "directive on a cache/prefetch hit. Cascade: override, per-agent ONLY " + "(never fleet-seeded from the defaults/profile tier — each flip is a " + "deliberate staggered M3 rollout step, same darkness safeguard as " + "rules_block). Default-true is the fail-safe direction under the " + "Zod-strip footgun: a stripped/misread key keeps the agent injecting."),
|
|
4183
4187
|
disposition: exports_external.object({
|
|
4184
4188
|
skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
|
|
4185
4189
|
literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
|
|
@@ -4625,6 +4629,7 @@ var init_schema = __esm(() => {
|
|
|
4625
4629
|
profile: exports_external.string().optional(),
|
|
4626
4630
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
4627
4631
|
profile_capture_nudge: exports_external.boolean().optional(),
|
|
4632
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional(),
|
|
4628
4633
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
4629
4634
|
observation_scopes: ObservationScopesSchema,
|
|
4630
4635
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
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.18",
|
|
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": {
|
|
@@ -1198,6 +1198,17 @@ export HINDSIGHT_DIRECTIVE_CAPTURE_NUDGE={{hindsightDirectiveCaptureNudge}}
|
|
|
1198
1198
|
{{#if hindsightProfileCaptureNudge}}
|
|
1199
1199
|
export HINDSIGHT_PROFILE_CAPTURE_NUDGE={{hindsightProfileCaptureNudge}}
|
|
1200
1200
|
{{/if}}
|
|
1201
|
+
# Memory v2 M3 Surface-A — directive-injection switch (memory.inject_directives
|
|
1202
|
+
# cascade). On by default (plugin settings.json): recall.py injects the
|
|
1203
|
+
# <active_directives> block every turn. Export only when the operator overrode
|
|
1204
|
+
# it; set false to SUPPRESS injection for this agent (a flipped M3 canary) — its
|
|
1205
|
+
# standing rules then come from the live rules block instead. recall.py keeps
|
|
1206
|
+
# the suppression fail-safe: it drops injection ONLY when a non-empty rules
|
|
1207
|
+
# block is physically present in CLAUDE.md, else it keeps injecting and emits a
|
|
1208
|
+
# degraded-canary notice. Flip via `switchroom memory flip`, never a bare edit.
|
|
1209
|
+
{{#if hindsightInjectDirectives}}
|
|
1210
|
+
export HINDSIGHT_INJECT_DIRECTIVES={{hindsightInjectDirectives}}
|
|
1211
|
+
{{/if}}
|
|
1201
1212
|
# Per-row observation scope (memory.observation_scopes cascade). Stamped on
|
|
1202
1213
|
# every retain — Stop hook, sidechain, boot reconcile, queue drain, backfill.
|
|
1203
1214
|
# "shared" makes consolidation pool this agent's observations into ONE global
|
|
@@ -21798,6 +21798,10 @@ var init_schema = __esm(() => {
|
|
|
21798
21798
|
}).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry \u2014 e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced \u2014 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded \u2014 each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
|
|
21799
21799
|
observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
|
|
21800
21800
|
rules_block: exports_external.boolean().default(false).describe("Memory v2 M3 go-live flag (M1 only DEFINES this, default false; " + "M3 flips it per agent). When true, the sanctioned rules/index " + "blocks render live in this agent's CLAUDE.md (marker-delimited, " + "below the `# --- Yours ---` line) AND the permission deny for " + "direct Edit/Write of the agent's own CLAUDE.md is seeded \u2014 the " + "flag couples deny + tools together so a live deny never orphans " + "the invited free-text edit path (the `memory_edit_yours` verb is " + "the only sanctioned writer once flipped). Unset/false \u21d2 byte-" + "identical to pre-M1 behaviour: no blocks, no deny, dark build. " + "Cascade: override (per-agent wins over default; never fleet-" + "seeded \u2014 each agent's flip is a deliberate M3 rollout step)."),
|
|
21801
|
+
orientation: exports_external.boolean().optional().describe("Memory v2 M5 go-live flag (Surface B: orientation-at-boot), " + "effective default false \u2014 dark build (unset \u21d2 OFF, applied by the " + "scaffold resolver, NOT a Zod .default() \u2014 a hard default here would " + "shadow the per-agent value in the cascade the way it does every other " + "memory knob). When true, the orientation SessionStart " + "hook injects this agent's cron-refreshed `orientation` mental model " + "into context at boot AND re-seats it after every compaction (E-88), " + "deterministically and with zero tool call. Unset/false \u21d2 byte-" + "identical to pre-M5 behaviour: the hook no-ops before any bank " + "resolve or network call. Per-agent ONLY (never fleet-seeded \u2014 a " + "flip is a deliberate canary/rollout step gated on the agent's " + "m2-residue measurement + operator-created model, carve-M5 \u00a77). " + "Cascade: override (per-agent wins over default)."),
|
|
21802
|
+
orientation_reinject_turns: exports_external.number().int().min(0).optional().describe("Memory v2 M5 optional per-turn re-inject cadence, effective default " + "0 = off (unset \u21d2 0, applied by the scaffold resolver, not a Zod " + ".default() \u2014 same cascade-shadowing reason as `orientation`). At 0 the " + "briefing is injected only at SessionStart + compaction (the cheap " + "path). N>0 " + "would additionally re-inject the orientation briefing every N turns; " + "priced at ~55M tok/30d at every-turn, which is WHY it defaults off " + "(carve-M5 \u00a72/\u00a74). Only late-session context loss should ever set it, " + "per-agent, measured. A stripped/absent value fails to 0 (fail-safe " + "AND fail-cheap). Per-agent ONLY. Cascade: override."),
|
|
21803
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional().describe("Memory v2 M5 per-agent refresh cadence tier (hours), effective " + "default 48 (unset \u21d2 48, applied by the scaffold resolver, not a Zod " + ".default() \u2014 a hard default would shadow a `defaults.memory." + "orientation_cadence_hours` fleet value in the cascade). " + "The fast-churning agents (klanker, overlord) set 24; everyone else " + "inherits 48 (carve-M5 \u00a76, Ken's locked tiered cadence). Drives BOTH " + "the out-of-session refresh scheduler AND the staleness guard's " + "per-tier thresholds (stale prefix at 1.5\u00d7 cadence, cold degrade at " + "3\u00d7) \u2014 so a fixed 36h can never mislabel a 48h-tier agent as stale a " + "day early. Modelled as an explicit int (not a hard-coded name list) " + "so the tiering is data-driven and auditable. A cost knob, not an " + "enablement knob: a stripped/absent value failing to 48 is safe " + "(the cheaper tier). Cascade: override (per-agent wins over default); " + "unlike `orientation`/`_reinject_turns` it IS accepted at the " + "defaults/profile tier."),
|
|
21804
|
+
inject_directives: exports_external.boolean().optional().describe("Memory v2 M3 Surface-A directive-injection switch (default TRUE \u2014 " + "on-by-default via the plugin settings.json stamp + config.py " + "DEFAULTS). When left unset the agent keeps injecting the " + "`<active_directives>` block on every turn, byte-identical to pre-M3. " + "Set FALSE to SUPPRESS that injection for this agent \u2014 its standing " + "rules are then served from the live rules block (memory.rules_block) " + "instead of re-injected directives, which is the change that collapses " + "the always-on directive-token spend (E-41). The suppression is " + "fail-safe and re-checked every turn in recall.py: it fires ONLY when " + "a non-empty rules block is physically present in the agent's " + "CLAUDE.md \u2014 a false flag with an empty/absent block keeps injecting " + "AND emits a degraded-canary notice, so a zero-standing-rules turn is " + "unreachable. Requires memory.rules_block=true first (the block must " + "be live before injection is turned off \u2014 enforce the ordering with " + "`switchroom memory flip`, never a bare flag flip). Coordinates with " + "M4's memory.recall prefetch (memoryPrefetchEnabled): the same guard " + "gates all three recall.py emit sites so a flipped agent leaks no " + "directive on a cache/prefetch hit. Cascade: override, per-agent ONLY " + "(never fleet-seeded from the defaults/profile tier \u2014 each flip is a " + "deliberate staggered M3 rollout step, same darkness safeguard as " + "rules_block). Default-true is the fail-safe direction under the " + "Zod-strip footgun: a stripped/misread key keeps the agent injecting."),
|
|
21801
21805
|
disposition: exports_external.object({
|
|
21802
21806
|
skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
|
|
21803
21807
|
literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
|
|
@@ -22243,6 +22247,7 @@ var init_schema = __esm(() => {
|
|
|
22243
22247
|
profile: exports_external.string().optional(),
|
|
22244
22248
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
22245
22249
|
profile_capture_nudge: exports_external.boolean().optional(),
|
|
22250
|
+
orientation_cadence_hours: exports_external.number().int().min(1).optional(),
|
|
22246
22251
|
anti_confabulation_directive: AntiConfabulationDirectiveSchema,
|
|
22247
22252
|
observation_scopes: ObservationScopesSchema,
|
|
22248
22253
|
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
@@ -105871,10 +105876,10 @@ function startOutboxSweep(deps) {
|
|
|
105871
105876
|
}
|
|
105872
105877
|
|
|
105873
105878
|
// ../src/build-info.ts
|
|
105874
|
-
var VERSION2 = "0.21.
|
|
105875
|
-
var COMMIT_SHA = "
|
|
105876
|
-
var COMMIT_DATE = "2026-08-
|
|
105877
|
-
var LATEST_PR =
|
|
105879
|
+
var VERSION2 = "0.21.18";
|
|
105880
|
+
var COMMIT_SHA = "ae063418";
|
|
105881
|
+
var COMMIT_DATE = "2026-08-18T03:14:53Z";
|
|
105882
|
+
var LATEST_PR = 4772;
|
|
105878
105883
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
105879
105884
|
|
|
105880
105885
|
// gateway/boot-version.ts
|
|
@@ -82,6 +82,12 @@ else
|
|
|
82
82
|
uat/load-env.test.ts
|
|
83
83
|
uat/uat-driver.test.ts
|
|
84
84
|
uat/runners/
|
|
85
|
+
# M3 directive-flip UAT — deterministic (no-model, no-network) gate half.
|
|
86
|
+
# Same rationale as uat/runners/: vitest excludes all of uat/**, and these
|
|
87
|
+
# import `vitest` (bun runs them via its vitest-compat shim) — so they run
|
|
88
|
+
# on the BUN side. Directory match (trailing slash) never widens into
|
|
89
|
+
# uat/scenarios/. Fenced by scripts/check-test-runner-coverage.mjs.
|
|
90
|
+
uat/flip/
|
|
85
91
|
)
|
|
86
92
|
fi
|
|
87
93
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit suite for the UAT allowlist add/remove helper. Runs under `bun test`
|
|
3
|
+
* (this tree is vitest-excluded) via the `uat/flip/` entry in
|
|
4
|
+
* telegram-plugin/scripts/bun-test-ci.sh.
|
|
5
|
+
*
|
|
6
|
+
* Everything is hermetic: a tmp `agentsDir` is built per-test and torn down,
|
|
7
|
+
* and the clock is injected so `expiresAt`/`--expired` are deterministic. No
|
|
8
|
+
* real ~/.switchroom is touched.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import {
|
|
23
|
+
addToAllowlist,
|
|
24
|
+
removeFromAllowlist,
|
|
25
|
+
sweepAllowlist,
|
|
26
|
+
runAllowlistCli,
|
|
27
|
+
accessFilePath,
|
|
28
|
+
receiptPath,
|
|
29
|
+
DEFAULT_TTL_MS,
|
|
30
|
+
type AllowlistReceipt,
|
|
31
|
+
} from "./allowlist.js";
|
|
32
|
+
|
|
33
|
+
let agentsDir: string;
|
|
34
|
+
let root: string;
|
|
35
|
+
|
|
36
|
+
function seedAccess(agent: string, obj: unknown): void {
|
|
37
|
+
const p = accessFilePath(agentsDir, agent);
|
|
38
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
39
|
+
writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readAccess(agent: string): Record<string, unknown> {
|
|
43
|
+
return JSON.parse(readFileSync(accessFilePath(agentsDir, agent), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readReceipt(agent: string): AllowlistReceipt {
|
|
47
|
+
return JSON.parse(readFileSync(receiptPath(agentsDir, agent), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mkAgentDir(agent: string): void {
|
|
51
|
+
mkdirSync(join(agentsDir, agent, "telegram"), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
root = mkdtempSync(join(tmpdir(), "sr-uat-allowlist-"));
|
|
56
|
+
agentsDir = join(root, "agents");
|
|
57
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
rmSync(root, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("addToAllowlist", () => {
|
|
65
|
+
it("appends the id, preserves sibling keys, and writes a receipt first", () => {
|
|
66
|
+
seedAccess("ziggy", {
|
|
67
|
+
dmPolicy: "allowlist",
|
|
68
|
+
allowFrom: ["111"],
|
|
69
|
+
groups: { "-100": { allowFrom: ["222"] } },
|
|
70
|
+
voice_in: true,
|
|
71
|
+
});
|
|
72
|
+
const now = Date.parse("2026-08-18T00:00:00.000Z");
|
|
73
|
+
const r = addToAllowlist("ziggy", "999", { agentsDir, now: () => now });
|
|
74
|
+
|
|
75
|
+
expect(r.added).toBe(true);
|
|
76
|
+
expect(r.preexisting).toBe(false);
|
|
77
|
+
|
|
78
|
+
const access = readAccess("ziggy");
|
|
79
|
+
expect(access.allowFrom).toEqual(["111", "999"]);
|
|
80
|
+
// sibling keys survive the whole-object rewrite.
|
|
81
|
+
expect(access.groups).toEqual({ "-100": { allowFrom: ["222"] } });
|
|
82
|
+
expect(access.voice_in).toBe(true);
|
|
83
|
+
expect(access.dmPolicy).toBe("allowlist");
|
|
84
|
+
|
|
85
|
+
const rcpt = readReceipt("ziggy");
|
|
86
|
+
expect(rcpt).toMatchObject({ agent: "ziggy", userId: "999", preexisting: false });
|
|
87
|
+
expect(rcpt.addedAt).toBe("2026-08-18T00:00:00.000Z");
|
|
88
|
+
expect(Date.parse(rcpt.expiresAt) - now).toBe(DEFAULT_TTL_MS);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("creates access.json when none exists yet", () => {
|
|
92
|
+
mkAgentDir("newbie");
|
|
93
|
+
const r = addToAllowlist("newbie", "42", { agentsDir });
|
|
94
|
+
expect(r.added).toBe(true);
|
|
95
|
+
expect(readAccess("newbie").allowFrom).toEqual(["42"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("is a no-op with preexisting:true when the id is already allowlisted", () => {
|
|
99
|
+
seedAccess("ziggy", { allowFrom: ["999"] });
|
|
100
|
+
const r = addToAllowlist("ziggy", "999", { agentsDir });
|
|
101
|
+
expect(r.added).toBe(false);
|
|
102
|
+
expect(r.preexisting).toBe(true);
|
|
103
|
+
// access.json unchanged; receipt records preexisting so revert won't strip it.
|
|
104
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["999"]);
|
|
105
|
+
expect(readReceipt("ziggy").preexisting).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("a second add is a no-op preserving the original receipt", () => {
|
|
109
|
+
seedAccess("ziggy", { allowFrom: [] });
|
|
110
|
+
const first = addToAllowlist("ziggy", "999", { agentsDir, now: () => 1000 });
|
|
111
|
+
const second = addToAllowlist("ziggy", "999", { agentsDir, now: () => 5000 });
|
|
112
|
+
expect(first.added).toBe(true);
|
|
113
|
+
expect(second.added).toBe(false);
|
|
114
|
+
// original addedAt preserved (not re-stamped to 5000).
|
|
115
|
+
expect(readReceipt("ziggy").addedAt).toBe(new Date(1000).toISOString());
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("removeFromAllowlist", () => {
|
|
120
|
+
it("reverts an id the UAT added and clears the receipt", () => {
|
|
121
|
+
seedAccess("ziggy", { allowFrom: ["111"] });
|
|
122
|
+
addToAllowlist("ziggy", "999", { agentsDir });
|
|
123
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["111", "999"]);
|
|
124
|
+
|
|
125
|
+
const r = removeFromAllowlist("ziggy", { agentsDir });
|
|
126
|
+
expect(r.reverted).toBe(true);
|
|
127
|
+
expect(r.receiptCleared).toBe(true);
|
|
128
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["111"]);
|
|
129
|
+
expect(existsSync(receiptPath(agentsDir, "ziggy"))).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does NOT strip a preexisting id, but clears the receipt", () => {
|
|
133
|
+
seedAccess("ziggy", { allowFrom: ["999"] });
|
|
134
|
+
addToAllowlist("ziggy", "999", { agentsDir }); // preexisting
|
|
135
|
+
const r = removeFromAllowlist("ziggy", { agentsDir });
|
|
136
|
+
expect(r.reverted).toBe(false);
|
|
137
|
+
expect(r.reason).toBe("preexisting");
|
|
138
|
+
expect(r.receiptCleared).toBe(true);
|
|
139
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["999"]);
|
|
140
|
+
expect(existsSync(receiptPath(agentsDir, "ziggy"))).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("no-ops with no-receipt when there is nothing to revert", () => {
|
|
144
|
+
seedAccess("ziggy", { allowFrom: ["111"] });
|
|
145
|
+
const r = removeFromAllowlist("ziggy", { agentsDir });
|
|
146
|
+
expect(r.reverted).toBe(false);
|
|
147
|
+
expect(r.reason).toBe("no-receipt");
|
|
148
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["111"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("is idempotent — a second remove after revert is a no-op", () => {
|
|
152
|
+
seedAccess("ziggy", { allowFrom: [] });
|
|
153
|
+
addToAllowlist("ziggy", "999", { agentsDir });
|
|
154
|
+
removeFromAllowlist("ziggy", { agentsDir });
|
|
155
|
+
const second = removeFromAllowlist("ziggy", { agentsDir });
|
|
156
|
+
expect(second.reverted).toBe(false);
|
|
157
|
+
expect(second.reason).toBe("no-receipt");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles a receipt whose id was already manually removed", () => {
|
|
161
|
+
seedAccess("ziggy", { allowFrom: [] });
|
|
162
|
+
addToAllowlist("ziggy", "999", { agentsDir });
|
|
163
|
+
// someone else stripped the id out of band.
|
|
164
|
+
seedAccess("ziggy", { allowFrom: [] });
|
|
165
|
+
const r = removeFromAllowlist("ziggy", { agentsDir });
|
|
166
|
+
expect(r.reverted).toBe(false);
|
|
167
|
+
expect(r.reason).toBe("not-present");
|
|
168
|
+
expect(r.receiptCleared).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("sweepAllowlist", () => {
|
|
173
|
+
it("reverts every agent's receipt and is idempotent on a clean second pass", () => {
|
|
174
|
+
seedAccess("a", { allowFrom: [] });
|
|
175
|
+
seedAccess("b", { allowFrom: ["7"] });
|
|
176
|
+
addToAllowlist("a", "999", { agentsDir });
|
|
177
|
+
addToAllowlist("b", "888", { agentsDir });
|
|
178
|
+
|
|
179
|
+
const first = sweepAllowlist({ agentsDir });
|
|
180
|
+
expect(first.map((r) => r.agent).sort()).toEqual(["a", "b"]);
|
|
181
|
+
expect(first.every((r) => r.reverted)).toBe(true);
|
|
182
|
+
expect(readAccess("a").allowFrom).toEqual([]);
|
|
183
|
+
expect(readAccess("b").allowFrom).toEqual(["7"]);
|
|
184
|
+
|
|
185
|
+
const second = sweepAllowlist({ agentsDir });
|
|
186
|
+
expect(second).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("with expiredOnly, leaves un-expired receipts alone", () => {
|
|
190
|
+
seedAccess("a", { allowFrom: [] });
|
|
191
|
+
seedAccess("b", { allowFrom: [] });
|
|
192
|
+
const t0 = Date.parse("2026-08-18T00:00:00.000Z");
|
|
193
|
+
addToAllowlist("a", "111", { agentsDir, now: () => t0, ttlMs: 1000 });
|
|
194
|
+
addToAllowlist("b", "222", { agentsDir, now: () => t0, ttlMs: 60 * 60 * 1000 });
|
|
195
|
+
|
|
196
|
+
// 2s later: a's receipt (ttl 1s) is expired, b's (ttl 1h) is not.
|
|
197
|
+
const swept = sweepAllowlist({ agentsDir, expiredOnly: true, now: () => t0 + 2000 });
|
|
198
|
+
expect(swept.map((r) => r.agent)).toEqual(["a"]);
|
|
199
|
+
expect(readAccess("a").allowFrom).toEqual([]);
|
|
200
|
+
// b untouched.
|
|
201
|
+
expect(readAccess("b").allowFrom).toEqual(["222"]);
|
|
202
|
+
expect(existsSync(receiptPath(agentsDir, "b"))).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns empty on a missing agents dir", () => {
|
|
206
|
+
expect(sweepAllowlist({ agentsDir: join(root, "nope") })).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("runAllowlistCli", () => {
|
|
211
|
+
it("add then remove --sweep round-trips to zero residue", () => {
|
|
212
|
+
seedAccess("ziggy", { allowFrom: ["1"] });
|
|
213
|
+
expect(runAllowlistCli(["add", "ziggy", "999", "--agents-dir", agentsDir])).toBe(0);
|
|
214
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["1", "999"]);
|
|
215
|
+
|
|
216
|
+
expect(runAllowlistCli(["remove", "--sweep", "--agents-dir", agentsDir])).toBe(0);
|
|
217
|
+
expect(readAccess("ziggy").allowFrom).toEqual(["1"]);
|
|
218
|
+
expect(existsSync(receiptPath(agentsDir, "ziggy"))).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("add without a userId is a usage error (exit 1)", () => {
|
|
222
|
+
expect(runAllowlistCli(["add", "ziggy", "--agents-dir", agentsDir])).toBe(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("unknown command is exit 1; bare invocation is exit 0", () => {
|
|
226
|
+
expect(runAllowlistCli(["frobnicate"])).toBe(1);
|
|
227
|
+
expect(runAllowlistCli([])).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
});
|