switchroom 0.15.16 → 0.15.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 +4 -12
- package/dist/auth-broker/index.js +2 -10
- package/dist/cli/notion-write-pretool.mjs +2 -10
- package/dist/cli/switchroom.js +217 -36
- package/dist/host-control/main.js +2 -10
- package/dist/vault/approvals/kernel-server.js +3 -11
- package/dist/vault/broker/server.js +3 -11
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +33 -83
- package/telegram-plugin/gateway/gateway.ts +61 -84
- package/telegram-plugin/scoped-approval.ts +14 -11
- package/telegram-plugin/tests/scoped-approval.test.ts +2 -2
|
@@ -10978,16 +10978,8 @@ var HttpDiffPollSchema = exports_external.object({
|
|
|
10978
10978
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
10979
10979
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
10980
10980
|
});
|
|
10981
|
-
var TelegramReactionsPollSchema = exports_external.object({
|
|
10982
|
-
type: exports_external.literal("telegram-reactions"),
|
|
10983
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
10984
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
10985
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
10986
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
10987
|
-
});
|
|
10988
10981
|
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
10989
|
-
HttpDiffPollSchema
|
|
10990
|
-
TelegramReactionsPollSchema
|
|
10982
|
+
HttpDiffPollSchema
|
|
10991
10983
|
]);
|
|
10992
10984
|
var TelegramMessageActionSchema = exports_external.object({
|
|
10993
10985
|
type: exports_external.literal("telegram-message"),
|
|
@@ -11025,7 +11017,7 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11025
11017
|
ctx.addIssue({
|
|
11026
11018
|
code: exports_external.ZodIssueCode.custom,
|
|
11027
11019
|
path: ["poll"],
|
|
11028
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
11020
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
11029
11021
|
});
|
|
11030
11022
|
}
|
|
11031
11023
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -12432,7 +12424,7 @@ function estimateCronGapMin(expr) {
|
|
|
12432
12424
|
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
12433
12425
|
function resolveCronAutoTier(env = process.env) {
|
|
12434
12426
|
const v = (env.SWITCHROOM_CRON_AUTO_TIER ?? "").toLowerCase();
|
|
12435
|
-
return v === "
|
|
12427
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
12436
12428
|
}
|
|
12437
12429
|
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12438
12430
|
if (input.kind === "action") {
|
|
@@ -13315,7 +13307,7 @@ function buildCheapCronHooks(config, env, deps = {}) {
|
|
|
13315
13307
|
now
|
|
13316
13308
|
});
|
|
13317
13309
|
}
|
|
13318
|
-
return { hit: false, baseline: false, error:
|
|
13310
|
+
return { hit: false, baseline: false, error: "poll type not yet wired" };
|
|
13319
13311
|
};
|
|
13320
13312
|
const runAction2 = async (spec, ctx) => {
|
|
13321
13313
|
return runAction(spec, {
|
|
@@ -10978,16 +10978,8 @@ var HttpDiffPollSchema = exports_external.object({
|
|
|
10978
10978
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
10979
10979
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
10980
10980
|
});
|
|
10981
|
-
var TelegramReactionsPollSchema = exports_external.object({
|
|
10982
|
-
type: exports_external.literal("telegram-reactions"),
|
|
10983
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
10984
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
10985
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
10986
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
10987
|
-
});
|
|
10988
10981
|
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
10989
|
-
HttpDiffPollSchema
|
|
10990
|
-
TelegramReactionsPollSchema
|
|
10982
|
+
HttpDiffPollSchema
|
|
10991
10983
|
]);
|
|
10992
10984
|
var TelegramMessageActionSchema = exports_external.object({
|
|
10993
10985
|
type: exports_external.literal("telegram-message"),
|
|
@@ -11025,7 +11017,7 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11025
11017
|
ctx.addIssue({
|
|
11026
11018
|
code: exports_external.ZodIssueCode.custom,
|
|
11027
11019
|
path: ["poll"],
|
|
11028
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
11020
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
11029
11021
|
});
|
|
11030
11022
|
}
|
|
11031
11023
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -11726,16 +11726,8 @@ var HttpDiffPollSchema = exports_external.object({
|
|
|
11726
11726
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11727
11727
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11728
11728
|
});
|
|
11729
|
-
var TelegramReactionsPollSchema = exports_external.object({
|
|
11730
|
-
type: exports_external.literal("telegram-reactions"),
|
|
11731
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
11732
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
11733
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11734
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11735
|
-
});
|
|
11736
11729
|
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11737
|
-
HttpDiffPollSchema
|
|
11738
|
-
TelegramReactionsPollSchema
|
|
11730
|
+
HttpDiffPollSchema
|
|
11739
11731
|
]);
|
|
11740
11732
|
var TelegramMessageActionSchema = exports_external.object({
|
|
11741
11733
|
type: exports_external.literal("telegram-message"),
|
|
@@ -11773,7 +11765,7 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
11773
11765
|
ctx.addIssue({
|
|
11774
11766
|
code: exports_external.ZodIssueCode.custom,
|
|
11775
11767
|
path: ["poll"],
|
|
11776
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
11768
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
11777
11769
|
});
|
|
11778
11770
|
}
|
|
11779
11771
|
if (kind !== "poll" && entry.poll) {
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
|
|
|
13520
13520
|
});
|
|
13521
13521
|
|
|
13522
13522
|
// src/config/schema.ts
|
|
13523
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema,
|
|
13523
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
13524
13524
|
var init_schema = __esm(() => {
|
|
13525
13525
|
init_zod();
|
|
13526
13526
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -13542,16 +13542,8 @@ var init_schema = __esm(() => {
|
|
|
13542
13542
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
13543
13543
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
13544
13544
|
});
|
|
13545
|
-
TelegramReactionsPollSchema = exports_external.object({
|
|
13546
|
-
type: exports_external.literal("telegram-reactions"),
|
|
13547
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
13548
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
13549
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
13550
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
13551
|
-
});
|
|
13552
13545
|
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13553
|
-
HttpDiffPollSchema
|
|
13554
|
-
TelegramReactionsPollSchema
|
|
13546
|
+
HttpDiffPollSchema
|
|
13555
13547
|
]);
|
|
13556
13548
|
TelegramMessageActionSchema = exports_external.object({
|
|
13557
13549
|
type: exports_external.literal("telegram-message"),
|
|
@@ -13589,7 +13581,7 @@ var init_schema = __esm(() => {
|
|
|
13589
13581
|
ctx.addIssue({
|
|
13590
13582
|
code: exports_external.ZodIssueCode.custom,
|
|
13591
13583
|
path: ["poll"],
|
|
13592
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
13584
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
13593
13585
|
});
|
|
13594
13586
|
}
|
|
13595
13587
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -15551,7 +15543,7 @@ function estimateCronGapMin(expr) {
|
|
|
15551
15543
|
// src/scheduler/tier-selector.ts
|
|
15552
15544
|
function resolveCronAutoTier(env2 = process.env) {
|
|
15553
15545
|
const v = (env2.SWITCHROOM_CRON_AUTO_TIER ?? "").toLowerCase();
|
|
15554
|
-
return v === "
|
|
15546
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
15555
15547
|
}
|
|
15556
15548
|
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15557
15549
|
if (input.kind === "action") {
|
|
@@ -49579,9 +49571,9 @@ __export(exports_server, {
|
|
|
49579
49571
|
dispatchTool: () => dispatchTool,
|
|
49580
49572
|
TOOLS: () => TOOLS
|
|
49581
49573
|
});
|
|
49582
|
-
import { spawnSync as
|
|
49574
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
49583
49575
|
function execCli(args, stdin) {
|
|
49584
|
-
const r =
|
|
49576
|
+
const r = spawnSync14(CLI_BIN, args, {
|
|
49585
49577
|
encoding: "utf-8",
|
|
49586
49578
|
env: process.env,
|
|
49587
49579
|
timeout: 15000,
|
|
@@ -50467,8 +50459,8 @@ var {
|
|
|
50467
50459
|
} = import__.default;
|
|
50468
50460
|
|
|
50469
50461
|
// src/build-info.ts
|
|
50470
|
-
var VERSION = "0.15.
|
|
50471
|
-
var COMMIT_SHA = "
|
|
50462
|
+
var VERSION = "0.15.18";
|
|
50463
|
+
var COMMIT_SHA = "d7c044b9";
|
|
50472
50464
|
|
|
50473
50465
|
// src/cli/agent.ts
|
|
50474
50466
|
init_source();
|
|
@@ -76817,6 +76809,194 @@ function registerUpdateCommand(program3) {
|
|
|
76817
76809
|
});
|
|
76818
76810
|
}
|
|
76819
76811
|
|
|
76812
|
+
// src/cli/rollout.ts
|
|
76813
|
+
init_helpers();
|
|
76814
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
76815
|
+
function normalizeVersion(v) {
|
|
76816
|
+
return v.trim().replace(/^v/, "");
|
|
76817
|
+
}
|
|
76818
|
+
function isVersionAssertable(target) {
|
|
76819
|
+
return /^v?\d+\.\d+\.\d+$/.test(target.trim());
|
|
76820
|
+
}
|
|
76821
|
+
function orderAgentsCanaryFirst(agents) {
|
|
76822
|
+
const canary = agents.filter((a) => a === "test-harness");
|
|
76823
|
+
const rest = agents.filter((a) => a !== "test-harness");
|
|
76824
|
+
return [...canary, ...rest];
|
|
76825
|
+
}
|
|
76826
|
+
function planRollout(agents, opts = {}) {
|
|
76827
|
+
const steps = [{ kind: "apply" }];
|
|
76828
|
+
for (const agent of orderAgentsCanaryFirst(agents)) {
|
|
76829
|
+
steps.push({ kind: "restart-agent", agent });
|
|
76830
|
+
}
|
|
76831
|
+
if (!opts.skipWeb) {
|
|
76832
|
+
steps.push({ kind: "refresh-web" });
|
|
76833
|
+
steps.push({ kind: "refresh-hostd" });
|
|
76834
|
+
}
|
|
76835
|
+
steps.push({ kind: "sweep" });
|
|
76836
|
+
return steps;
|
|
76837
|
+
}
|
|
76838
|
+
function formatRolloutPlan(steps, target) {
|
|
76839
|
+
const lines = [`Rollout plan \u2192 ${target}:`];
|
|
76840
|
+
let n = 0;
|
|
76841
|
+
for (const s of steps) {
|
|
76842
|
+
n += 1;
|
|
76843
|
+
switch (s.kind) {
|
|
76844
|
+
case "apply":
|
|
76845
|
+
lines.push(` ${n}. apply \u2014 regenerate compose with ${target} image refs`);
|
|
76846
|
+
break;
|
|
76847
|
+
case "restart-agent":
|
|
76848
|
+
lines.push(` ${n}. restart ${s.agent} (--wait --force) + assert --version=${target}`);
|
|
76849
|
+
break;
|
|
76850
|
+
case "refresh-web":
|
|
76851
|
+
lines.push(` ${n}. webd install --tag ${target} (separate compose project)`);
|
|
76852
|
+
break;
|
|
76853
|
+
case "refresh-hostd":
|
|
76854
|
+
lines.push(` ${n}. hostd install --tag ${target} (separate compose project)`);
|
|
76855
|
+
break;
|
|
76856
|
+
case "sweep":
|
|
76857
|
+
lines.push(` ${n}. sweep \u2014 print per-agent version table`);
|
|
76858
|
+
break;
|
|
76859
|
+
}
|
|
76860
|
+
}
|
|
76861
|
+
lines.push("");
|
|
76862
|
+
lines.push("Stops on the first agent that doesn't come back on the target version.");
|
|
76863
|
+
return lines.join(`
|
|
76864
|
+
`);
|
|
76865
|
+
}
|
|
76866
|
+
function executeRollout(steps, target, deps, pinOnApply) {
|
|
76867
|
+
const targetNorm = normalizeVersion(target);
|
|
76868
|
+
const rolled = [];
|
|
76869
|
+
const warnings = [];
|
|
76870
|
+
for (const step of steps) {
|
|
76871
|
+
switch (step.kind) {
|
|
76872
|
+
case "apply": {
|
|
76873
|
+
deps.log(`\u2192 apply \u2014 regenerating compose for ${target}`);
|
|
76874
|
+
const args = pinOnApply ? ["apply", "--pin", target] : ["apply"];
|
|
76875
|
+
const r = deps.run(args);
|
|
76876
|
+
if (r.status !== 0) {
|
|
76877
|
+
return { ok: false, rolled, failedStep: "apply", warnings };
|
|
76878
|
+
}
|
|
76879
|
+
break;
|
|
76880
|
+
}
|
|
76881
|
+
case "restart-agent": {
|
|
76882
|
+
deps.log(`\u2192 restart ${step.agent} (--wait --force)`);
|
|
76883
|
+
deps.run(["agent", "restart", step.agent, "--wait", "--force"]);
|
|
76884
|
+
const got = deps.probeVersion(step.agent);
|
|
76885
|
+
if (got === null || normalizeVersion(got) !== targetNorm) {
|
|
76886
|
+
deps.log(` \u2717 ${step.agent} \u2192 ${got ?? "<unreachable>"} (expected ${target}) \u2014 STOPPING`);
|
|
76887
|
+
return {
|
|
76888
|
+
ok: false,
|
|
76889
|
+
rolled,
|
|
76890
|
+
failedStep: "restart-agent",
|
|
76891
|
+
failedAgent: step.agent,
|
|
76892
|
+
got,
|
|
76893
|
+
warnings
|
|
76894
|
+
};
|
|
76895
|
+
}
|
|
76896
|
+
rolled.push(step.agent);
|
|
76897
|
+
deps.log(` \u2713 ${step.agent} \u2192 ${got}`);
|
|
76898
|
+
break;
|
|
76899
|
+
}
|
|
76900
|
+
case "refresh-web": {
|
|
76901
|
+
deps.log(`\u2192 webd install --tag ${target}`);
|
|
76902
|
+
const r = deps.run(["webd", "install", "--tag", target]);
|
|
76903
|
+
if (r.status !== 0)
|
|
76904
|
+
warnings.push(`web refresh failed (non-fatal); agents already rolled`);
|
|
76905
|
+
break;
|
|
76906
|
+
}
|
|
76907
|
+
case "refresh-hostd": {
|
|
76908
|
+
deps.log(`\u2192 hostd install --tag ${target}`);
|
|
76909
|
+
const r = deps.run(["hostd", "install", "--tag", target]);
|
|
76910
|
+
if (r.status !== 0)
|
|
76911
|
+
warnings.push(`hostd refresh failed (non-fatal); agents already rolled`);
|
|
76912
|
+
break;
|
|
76913
|
+
}
|
|
76914
|
+
case "sweep": {
|
|
76915
|
+
deps.log(`\u2192 sweep`);
|
|
76916
|
+
for (const a of rolled) {
|
|
76917
|
+
const v = deps.probeVersion(a);
|
|
76918
|
+
deps.log(` ${a}: ${v ?? "<unreachable>"}`);
|
|
76919
|
+
}
|
|
76920
|
+
break;
|
|
76921
|
+
}
|
|
76922
|
+
}
|
|
76923
|
+
}
|
|
76924
|
+
return { ok: true, rolled, warnings };
|
|
76925
|
+
}
|
|
76926
|
+
function registerRolloutCommand(program3) {
|
|
76927
|
+
program3.command("rollout").description("Deploy a pinned version to the fleet, safely (staggered restart + " + "per-agent version assert + web/hostd refresh). Run with sudo.").option("--pin <version>", "Version to roll (e.g. v0.15.18). Defaults to release.pin from config.").option("--agents <list>", "Comma-separated subset of agents to roll (default: all configured).").option("--skip-web", "Skip the web + hostd refresh step.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
|
|
76928
|
+
const config = getConfig(program3);
|
|
76929
|
+
const target = opts.pin ?? config.release?.pin;
|
|
76930
|
+
if (!target) {
|
|
76931
|
+
process.stderr.write("rollout needs a pinned version: pass --pin vX.Y.Z, or set " + "`release.pin` in switchroom.yaml. (A floating channel has no " + `fixed version to assert against.)
|
|
76932
|
+
`);
|
|
76933
|
+
process.exitCode = 2;
|
|
76934
|
+
return;
|
|
76935
|
+
}
|
|
76936
|
+
if (!isVersionAssertable(target)) {
|
|
76937
|
+
process.stderr.write(`rollout asserts the in-container \`switchroom --version\` (always a ` + `semver), so the target must be a tagged release like v0.15.18 \u2014 ` + `\`${target}\` isn't version-assertable. Pass --pin vX.Y.Z.
|
|
76938
|
+
`);
|
|
76939
|
+
process.exitCode = 2;
|
|
76940
|
+
return;
|
|
76941
|
+
}
|
|
76942
|
+
const allAgents = Object.keys(config.agents ?? {});
|
|
76943
|
+
const requested = opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : allAgents;
|
|
76944
|
+
const unknown = requested.filter((a) => !allAgents.includes(a));
|
|
76945
|
+
if (unknown.length > 0) {
|
|
76946
|
+
process.stderr.write(`unknown agent(s): ${unknown.join(", ")}
|
|
76947
|
+
`);
|
|
76948
|
+
process.exitCode = 2;
|
|
76949
|
+
return;
|
|
76950
|
+
}
|
|
76951
|
+
if (requested.length === 0) {
|
|
76952
|
+
process.stderr.write(`no agents to roll.
|
|
76953
|
+
`);
|
|
76954
|
+
process.exitCode = 2;
|
|
76955
|
+
return;
|
|
76956
|
+
}
|
|
76957
|
+
const steps = planRollout(requested, { skipWeb: opts.skipWeb });
|
|
76958
|
+
if (opts.dryRun) {
|
|
76959
|
+
process.stdout.write(formatRolloutPlan(steps, target) + `
|
|
76960
|
+
`);
|
|
76961
|
+
return;
|
|
76962
|
+
}
|
|
76963
|
+
const scriptPath = process.argv[1] ?? "switchroom";
|
|
76964
|
+
const deps = {
|
|
76965
|
+
run: (args) => {
|
|
76966
|
+
const r = spawnSync10(process.execPath, [scriptPath, ...args], { stdio: "inherit" });
|
|
76967
|
+
return { status: r.status ?? 1 };
|
|
76968
|
+
},
|
|
76969
|
+
probeVersion: (agent) => {
|
|
76970
|
+
const r = spawnSync10("docker", ["exec", `switchroom-${agent}`, "sh", "-lc", "switchroom --version"], { encoding: "utf8" });
|
|
76971
|
+
if (r.status !== 0)
|
|
76972
|
+
return null;
|
|
76973
|
+
return (r.stdout ?? "").trim().split(`
|
|
76974
|
+
`).pop()?.trim() ?? null;
|
|
76975
|
+
},
|
|
76976
|
+
log: (line) => process.stdout.write(line + `
|
|
76977
|
+
`)
|
|
76978
|
+
};
|
|
76979
|
+
process.stdout.write(`Rolling ${requested.length} agent(s) to ${target}\u2026
|
|
76980
|
+
`);
|
|
76981
|
+
const result = executeRollout(steps, target, deps, opts.pin != null);
|
|
76982
|
+
for (const w of result.warnings)
|
|
76983
|
+
process.stderr.write(`\u26a0\ufe0f ${w}
|
|
76984
|
+
`);
|
|
76985
|
+
if (!result.ok) {
|
|
76986
|
+
process.stderr.write(`
|
|
76987
|
+
\u2717 Rollout STOPPED at ${result.failedStep}` + (result.failedAgent ? ` (${result.failedAgent} \u2192 ${result.got ?? "unreachable"})` : "") + `.
|
|
76988
|
+
Rolled before stop: ${result.rolled.join(", ") || "none"}.
|
|
76989
|
+
` + ` Fix the cause, then re-run \`switchroom rollout --pin ${target}\` ` + `(idempotent \u2014 already-current agents bounce back to the same version).
|
|
76990
|
+
`);
|
|
76991
|
+
process.exitCode = 1;
|
|
76992
|
+
return;
|
|
76993
|
+
}
|
|
76994
|
+
process.stdout.write(`
|
|
76995
|
+
\u2705 Rollout complete \u2014 ${result.rolled.length} agent(s) on ${target}` + `${result.warnings.length ? ` (with ${result.warnings.length} warning(s) above)` : ""}.
|
|
76996
|
+
`);
|
|
76997
|
+
});
|
|
76998
|
+
}
|
|
76999
|
+
|
|
76820
77000
|
// src/cli/restart.ts
|
|
76821
77001
|
init_source();
|
|
76822
77002
|
init_helpers();
|
|
@@ -78272,7 +78452,7 @@ init_helpers();
|
|
|
78272
78452
|
init_loader();
|
|
78273
78453
|
import { existsSync as existsSync64 } from "node:fs";
|
|
78274
78454
|
import { resolve as resolve39, sep as sep3 } from "node:path";
|
|
78275
|
-
import { spawnSync as
|
|
78455
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
78276
78456
|
|
|
78277
78457
|
// src/agents/workspace.ts
|
|
78278
78458
|
import { readFile as readFile2, stat } from "node:fs/promises";
|
|
@@ -78985,7 +79165,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
78985
79165
|
process.exit(1);
|
|
78986
79166
|
}
|
|
78987
79167
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
|
|
78988
|
-
const child =
|
|
79168
|
+
const child = spawnSync11(editor, [target], { stdio: "inherit" });
|
|
78989
79169
|
if (child.status !== 0 && child.status !== null) {
|
|
78990
79170
|
process.exit(child.status);
|
|
78991
79171
|
}
|
|
@@ -79052,7 +79232,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
79052
79232
|
`);
|
|
79053
79233
|
return;
|
|
79054
79234
|
}
|
|
79055
|
-
const statusResult =
|
|
79235
|
+
const statusResult = spawnSync11("git", ["status", "--short"], {
|
|
79056
79236
|
cwd: dir,
|
|
79057
79237
|
encoding: "utf-8"
|
|
79058
79238
|
});
|
|
@@ -79067,7 +79247,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
79067
79247
|
return;
|
|
79068
79248
|
}
|
|
79069
79249
|
const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
|
|
79070
|
-
const addResult =
|
|
79250
|
+
const addResult = spawnSync11("git", ["add", "-A"], {
|
|
79071
79251
|
cwd: dir,
|
|
79072
79252
|
encoding: "utf-8"
|
|
79073
79253
|
});
|
|
@@ -79076,7 +79256,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
79076
79256
|
`);
|
|
79077
79257
|
process.exit(1);
|
|
79078
79258
|
}
|
|
79079
|
-
const commitResult =
|
|
79259
|
+
const commitResult = spawnSync11("git", ["commit", "-m", message], {
|
|
79080
79260
|
cwd: dir,
|
|
79081
79261
|
encoding: "utf-8"
|
|
79082
79262
|
});
|
|
@@ -79085,7 +79265,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
79085
79265
|
`);
|
|
79086
79266
|
process.exit(1);
|
|
79087
79267
|
}
|
|
79088
|
-
const shaResult =
|
|
79268
|
+
const shaResult = spawnSync11("git", ["rev-parse", "--short", "HEAD"], {
|
|
79089
79269
|
cwd: dir,
|
|
79090
79270
|
encoding: "utf-8"
|
|
79091
79271
|
});
|
|
@@ -79106,7 +79286,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
79106
79286
|
`);
|
|
79107
79287
|
return;
|
|
79108
79288
|
}
|
|
79109
|
-
const child =
|
|
79289
|
+
const child = spawnSync11("git", ["status", "--short"], {
|
|
79110
79290
|
cwd: dir,
|
|
79111
79291
|
stdio: "inherit"
|
|
79112
79292
|
});
|
|
@@ -84101,7 +84281,7 @@ import {
|
|
|
84101
84281
|
} from "node:fs";
|
|
84102
84282
|
import { tmpdir as tmpdir5, homedir as homedir45 } from "node:os";
|
|
84103
84283
|
import { dirname as dirname23, join as join79, relative as relative2, resolve as resolve47 } from "node:path";
|
|
84104
|
-
import { spawnSync as
|
|
84284
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
84105
84285
|
|
|
84106
84286
|
// src/cli/skill-common.ts
|
|
84107
84287
|
var import_yaml22 = __toESM(require_dist(), 1);
|
|
@@ -84354,7 +84534,7 @@ function loadFromDir(dir) {
|
|
|
84354
84534
|
function loadFromTarball(tarPath) {
|
|
84355
84535
|
const isGz = tarPath.endsWith(".gz") || tarPath.endsWith(".tgz");
|
|
84356
84536
|
const listFlags = isGz ? ["-tzf"] : ["-tf"];
|
|
84357
|
-
const list2 =
|
|
84537
|
+
const list2 = spawnSync12("tar", [...listFlags, tarPath], {
|
|
84358
84538
|
encoding: "utf-8",
|
|
84359
84539
|
stdio: ["ignore", "pipe", "pipe"]
|
|
84360
84540
|
});
|
|
@@ -84371,7 +84551,7 @@ function loadFromTarball(tarPath) {
|
|
|
84371
84551
|
const staging = mkdtempSync5(join79(tmpdir5(), "skill-apply-extract-"));
|
|
84372
84552
|
try {
|
|
84373
84553
|
const flags = isGz ? ["-xzf"] : ["-xf"];
|
|
84374
|
-
const r =
|
|
84554
|
+
const r = spawnSync12("tar", [
|
|
84375
84555
|
...flags,
|
|
84376
84556
|
tarPath,
|
|
84377
84557
|
"-C",
|
|
@@ -84446,7 +84626,7 @@ function validatePayload(name, files) {
|
|
|
84446
84626
|
if (errors2.length === 0) {
|
|
84447
84627
|
for (const [path8, content] of Object.entries(files)) {
|
|
84448
84628
|
if (SH_SCRIPT_RE2.test(path8)) {
|
|
84449
|
-
const r =
|
|
84629
|
+
const r = spawnSync12("bash", ["-n"], {
|
|
84450
84630
|
input: content,
|
|
84451
84631
|
encoding: "utf-8"
|
|
84452
84632
|
});
|
|
@@ -84458,7 +84638,7 @@ function validatePayload(name, files) {
|
|
|
84458
84638
|
const tmpPy = join79(tmp, "check.py");
|
|
84459
84639
|
try {
|
|
84460
84640
|
writeFileSync39(tmpPy, content);
|
|
84461
|
-
const r =
|
|
84641
|
+
const r = spawnSync12("python3", ["-m", "py_compile", tmpPy], {
|
|
84462
84642
|
encoding: "utf-8"
|
|
84463
84643
|
});
|
|
84464
84644
|
if (r.status !== 0) {
|
|
@@ -84621,7 +84801,7 @@ function registerSkillCommand(program3) {
|
|
|
84621
84801
|
\u2713 Wrote ${name} to ${currentDir}`));
|
|
84622
84802
|
const applyBin = process.argv[1] ?? "switchroom";
|
|
84623
84803
|
console.log(source_default.gray(`Running \`switchroom apply --non-interactive\`...`));
|
|
84624
|
-
const r =
|
|
84804
|
+
const r = spawnSync12(process.argv0, [applyBin, "apply", "--non-interactive"], { stdio: "inherit" });
|
|
84625
84805
|
if (r.status !== 0) {
|
|
84626
84806
|
console.error(source_default.yellow(`(warning: \`switchroom apply\` exited ${r.status} \u2014 skill is ` + `in the pool but symlinks may not be refreshed. Re-run manually.)`));
|
|
84627
84807
|
}
|
|
@@ -84653,7 +84833,7 @@ import {
|
|
|
84653
84833
|
} from "node:fs";
|
|
84654
84834
|
import { dirname as dirname24, join as join80, relative as relative3, resolve as resolve48 } from "node:path";
|
|
84655
84835
|
import { homedir as homedir46, tmpdir as tmpdir6 } from "node:os";
|
|
84656
|
-
import { spawnSync as
|
|
84836
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
84657
84837
|
init_helpers();
|
|
84658
84838
|
init_source();
|
|
84659
84839
|
var PERSONAL_PREFIX = "personal-";
|
|
@@ -84842,7 +85022,7 @@ function behavioralValidate(files) {
|
|
|
84842
85022
|
const errors2 = [];
|
|
84843
85023
|
for (const [path8, content] of Object.entries(files)) {
|
|
84844
85024
|
if (SH_SCRIPT_RE.test(path8)) {
|
|
84845
|
-
const r =
|
|
85025
|
+
const r = spawnSync13("bash", ["-n"], { input: content, encoding: "utf-8" });
|
|
84846
85026
|
if (r.status !== 0) {
|
|
84847
85027
|
errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
|
|
84848
85028
|
}
|
|
@@ -84851,7 +85031,7 @@ function behavioralValidate(files) {
|
|
|
84851
85031
|
const tmpPy = join80(tmp, "check.py");
|
|
84852
85032
|
try {
|
|
84853
85033
|
writeFileSync40(tmpPy, content);
|
|
84854
|
-
const r =
|
|
85034
|
+
const r = spawnSync13("python3", ["-m", "py_compile", tmpPy], {
|
|
84855
85035
|
encoding: "utf-8"
|
|
84856
85036
|
});
|
|
84857
85037
|
if (r.status !== 0) {
|
|
@@ -85544,7 +85724,7 @@ init_helpers();
|
|
|
85544
85724
|
import { existsSync as existsSync84, mkdirSync as mkdirSync47, readdirSync as readdirSync33, readFileSync as readFileSync71, writeFileSync as writeFileSync41, statSync as statSync34, copyFileSync as copyFileSync12 } from "node:fs";
|
|
85545
85725
|
import { homedir as homedir48 } from "node:os";
|
|
85546
85726
|
import { join as join82 } from "node:path";
|
|
85547
|
-
import { spawnSync as
|
|
85727
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
85548
85728
|
init_audit_reader();
|
|
85549
85729
|
function resolveHostdImageTag(explicitTag, release) {
|
|
85550
85730
|
if (explicitTag)
|
|
@@ -85676,7 +85856,7 @@ function backupExistingCompose() {
|
|
|
85676
85856
|
return bak;
|
|
85677
85857
|
}
|
|
85678
85858
|
function runDocker(args) {
|
|
85679
|
-
const r =
|
|
85859
|
+
const r = spawnSync15("docker", args, { encoding: "utf8" });
|
|
85680
85860
|
return {
|
|
85681
85861
|
ok: r.status === 0,
|
|
85682
85862
|
stdout: r.stdout ?? "",
|
|
@@ -85869,7 +86049,7 @@ init_helpers();
|
|
|
85869
86049
|
import { existsSync as existsSync85, mkdirSync as mkdirSync48, writeFileSync as writeFileSync42, copyFileSync as copyFileSync13 } from "node:fs";
|
|
85870
86050
|
import { homedir as homedir49 } from "node:os";
|
|
85871
86051
|
import { join as join83 } from "node:path";
|
|
85872
|
-
import { spawnSync as
|
|
86052
|
+
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
85873
86053
|
function resolveWebImageTag(explicitTag, release) {
|
|
85874
86054
|
if (explicitTag)
|
|
85875
86055
|
return explicitTag;
|
|
@@ -85968,7 +86148,7 @@ function backupExistingCompose2() {
|
|
|
85968
86148
|
return bak;
|
|
85969
86149
|
}
|
|
85970
86150
|
function runDocker2(args) {
|
|
85971
|
-
const r =
|
|
86151
|
+
const r = spawnSync16("docker", args, { encoding: "utf8" });
|
|
85972
86152
|
return {
|
|
85973
86153
|
ok: r.status === 0,
|
|
85974
86154
|
stdout: r.stdout ?? "",
|
|
@@ -86098,6 +86278,7 @@ var program3 = new Command().name("switchroom").description("Multi-agent orchest
|
|
|
86098
86278
|
registerSetupCommand(program3);
|
|
86099
86279
|
registerDoctorCommand(program3);
|
|
86100
86280
|
registerUpdateCommand(program3);
|
|
86281
|
+
registerRolloutCommand(program3);
|
|
86101
86282
|
registerRestartCommand(program3);
|
|
86102
86283
|
registerVersionCommand(program3);
|
|
86103
86284
|
registerVersionsCommand(program3);
|
|
@@ -13713,16 +13713,8 @@ var HttpDiffPollSchema = exports_external.object({
|
|
|
13713
13713
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
13714
13714
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
13715
13715
|
});
|
|
13716
|
-
var TelegramReactionsPollSchema = exports_external.object({
|
|
13717
|
-
type: exports_external.literal("telegram-reactions"),
|
|
13718
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
13719
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
13720
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
13721
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
13722
|
-
});
|
|
13723
13716
|
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13724
|
-
HttpDiffPollSchema
|
|
13725
|
-
TelegramReactionsPollSchema
|
|
13717
|
+
HttpDiffPollSchema
|
|
13726
13718
|
]);
|
|
13727
13719
|
var TelegramMessageActionSchema = exports_external.object({
|
|
13728
13720
|
type: exports_external.literal("telegram-message"),
|
|
@@ -13760,7 +13752,7 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
13760
13752
|
ctx.addIssue({
|
|
13761
13753
|
code: exports_external.ZodIssueCode.custom,
|
|
13762
13754
|
path: ["poll"],
|
|
13763
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
13755
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
13764
13756
|
});
|
|
13765
13757
|
}
|
|
13766
13758
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -11299,7 +11299,7 @@ var init_dist = __esm(() => {
|
|
|
11299
11299
|
});
|
|
11300
11300
|
|
|
11301
11301
|
// src/config/schema.ts
|
|
11302
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema,
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11303
11303
|
var init_schema = __esm(() => {
|
|
11304
11304
|
init_zod();
|
|
11305
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11321,16 +11321,8 @@ var init_schema = __esm(() => {
|
|
|
11321
11321
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11322
11322
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11323
11323
|
});
|
|
11324
|
-
TelegramReactionsPollSchema = exports_external.object({
|
|
11325
|
-
type: exports_external.literal("telegram-reactions"),
|
|
11326
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
11327
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
11328
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11329
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11330
|
-
});
|
|
11331
11324
|
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11332
|
-
HttpDiffPollSchema
|
|
11333
|
-
TelegramReactionsPollSchema
|
|
11325
|
+
HttpDiffPollSchema
|
|
11334
11326
|
]);
|
|
11335
11327
|
TelegramMessageActionSchema = exports_external.object({
|
|
11336
11328
|
type: exports_external.literal("telegram-message"),
|
|
@@ -11368,7 +11360,7 @@ var init_schema = __esm(() => {
|
|
|
11368
11360
|
ctx.addIssue({
|
|
11369
11361
|
code: exports_external.ZodIssueCode.custom,
|
|
11370
11362
|
path: ["poll"],
|
|
11371
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
11363
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
11372
11364
|
});
|
|
11373
11365
|
}
|
|
11374
11366
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -11299,7 +11299,7 @@ var init_zod = __esm(() => {
|
|
|
11299
11299
|
});
|
|
11300
11300
|
|
|
11301
11301
|
// src/config/schema.ts
|
|
11302
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema,
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11303
11303
|
var init_schema = __esm(() => {
|
|
11304
11304
|
init_zod();
|
|
11305
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11321,16 +11321,8 @@ var init_schema = __esm(() => {
|
|
|
11321
11321
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11322
11322
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11323
11323
|
});
|
|
11324
|
-
TelegramReactionsPollSchema = exports_external.object({
|
|
11325
|
-
type: exports_external.literal("telegram-reactions"),
|
|
11326
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
11327
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
11328
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11329
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11330
|
-
});
|
|
11331
11324
|
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11332
|
-
HttpDiffPollSchema
|
|
11333
|
-
TelegramReactionsPollSchema
|
|
11325
|
+
HttpDiffPollSchema
|
|
11334
11326
|
]);
|
|
11335
11327
|
TelegramMessageActionSchema = exports_external.object({
|
|
11336
11328
|
type: exports_external.literal("telegram-message"),
|
|
@@ -11368,7 +11360,7 @@ var init_schema = __esm(() => {
|
|
|
11368
11360
|
ctx.addIssue({
|
|
11369
11361
|
code: exports_external.ZodIssueCode.custom,
|
|
11370
11362
|
path: ["poll"],
|
|
11371
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
11363
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
11372
11364
|
});
|
|
11373
11365
|
}
|
|
11374
11366
|
if (kind !== "poll" && entry.poll) {
|
package/package.json
CHANGED
|
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
|
|
|
23802
23802
|
});
|
|
23803
23803
|
|
|
23804
23804
|
// ../src/config/schema.ts
|
|
23805
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema,
|
|
23805
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23806
23806
|
var init_schema = __esm(() => {
|
|
23807
23807
|
init_zod();
|
|
23808
23808
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -23824,16 +23824,8 @@ var init_schema = __esm(() => {
|
|
|
23824
23824
|
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
23825
23825
|
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
23826
23826
|
});
|
|
23827
|
-
TelegramReactionsPollSchema = exports_external.object({
|
|
23828
|
-
type: exports_external.literal("telegram-reactions"),
|
|
23829
|
-
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
23830
|
-
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
23831
|
-
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
23832
|
-
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
23833
|
-
});
|
|
23834
23827
|
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
23835
|
-
HttpDiffPollSchema
|
|
23836
|
-
TelegramReactionsPollSchema
|
|
23828
|
+
HttpDiffPollSchema
|
|
23837
23829
|
]);
|
|
23838
23830
|
TelegramMessageActionSchema = exports_external.object({
|
|
23839
23831
|
type: exports_external.literal("telegram-message"),
|
|
@@ -23871,7 +23863,7 @@ var init_schema = __esm(() => {
|
|
|
23871
23863
|
ctx.addIssue({
|
|
23872
23864
|
code: exports_external.ZodIssueCode.custom,
|
|
23873
23865
|
path: ["poll"],
|
|
23874
|
-
message: "kind: poll requires a `poll` spec (http-diff
|
|
23866
|
+
message: "kind: poll requires a `poll` spec (http-diff)."
|
|
23875
23867
|
});
|
|
23876
23868
|
}
|
|
23877
23869
|
if (kind !== "poll" && entry.poll) {
|
|
@@ -54166,11 +54158,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54166
54158
|
}
|
|
54167
54159
|
|
|
54168
54160
|
// ../src/build-info.ts
|
|
54169
|
-
var VERSION = "0.15.
|
|
54170
|
-
var COMMIT_SHA = "
|
|
54171
|
-
var COMMIT_DATE = "2026-06-
|
|
54172
|
-
var LATEST_PR =
|
|
54173
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
54161
|
+
var VERSION = "0.15.18";
|
|
54162
|
+
var COMMIT_SHA = "d7c044b9";
|
|
54163
|
+
var COMMIT_DATE = "2026-06-14T08:55:07+10:00";
|
|
54164
|
+
var LATEST_PR = null;
|
|
54165
|
+
var COMMITS_AHEAD_OF_TAG = 3;
|
|
54174
54166
|
|
|
54175
54167
|
// gateway/boot-version.ts
|
|
54176
54168
|
function formatRelativeAgo(iso) {
|
|
@@ -57214,10 +57206,8 @@ if (inboundSpool != null) {
|
|
|
57214
57206
|
}
|
|
57215
57207
|
}
|
|
57216
57208
|
var pendingPermissionBuffer = createPendingPermissionBuffer();
|
|
57217
|
-
function buildPermissionActionRow(requestId, showAlways
|
|
57218
|
-
const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow
|
|
57219
|
-
if (showTimeBox)
|
|
57220
|
-
kb.text("\u23F1 30 min", `perm:tmb:${requestId}`);
|
|
57209
|
+
function buildPermissionActionRow(requestId, showAlways) {
|
|
57210
|
+
const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
|
|
57221
57211
|
if (showAlways)
|
|
57222
57212
|
kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
|
|
57223
57213
|
return kb;
|
|
@@ -57473,10 +57463,8 @@ var ipcServer = createIpcServer({
|
|
|
57473
57463
|
description,
|
|
57474
57464
|
agentName: _client.agentName
|
|
57475
57465
|
});
|
|
57476
|
-
const
|
|
57477
|
-
const
|
|
57478
|
-
const showTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(toolName, inputPreview, scopeChoices) != null;
|
|
57479
|
-
const keyboard = buildPermissionActionRow(requestId, showAlways, showTimeBox);
|
|
57466
|
+
const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
|
|
57467
|
+
const keyboard = buildPermissionActionRow(requestId, showAlways);
|
|
57480
57468
|
const activeTurn = currentTurn;
|
|
57481
57469
|
const targets = resolvePermissionCardTargets();
|
|
57482
57470
|
for (const { chatId, threadId } of targets) {
|
|
@@ -65088,8 +65076,8 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
65088
65076
|
const cbMessageId = ctx.callbackQuery?.message?.message_id;
|
|
65089
65077
|
const metaForMessage = cbMessageId != null ? agentButtonMeta.get(`${cbChatId}:${cbMessageId}`) : undefined;
|
|
65090
65078
|
const tapMeta = metaForMessage?.get(agentCb.raw);
|
|
65091
|
-
const
|
|
65092
|
-
await ctx.answerCallbackQuery({ text:
|
|
65079
|
+
const ackText2 = typeof tapMeta?.ack_text === "string" && tapMeta.ack_text.length > 0 ? tapMeta.ack_text : "\u2713 received";
|
|
65080
|
+
await ctx.answerCallbackQuery({ text: ackText2 }).catch(() => {});
|
|
65093
65081
|
const buttonText = (() => {
|
|
65094
65082
|
const msg2 = ctx.callbackQuery?.message;
|
|
65095
65083
|
const kb = msg2 && "reply_markup" in msg2 ? msg2.reply_markup : undefined;
|
|
@@ -65158,7 +65146,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
65158
65146
|
}
|
|
65159
65147
|
return;
|
|
65160
65148
|
}
|
|
65161
|
-
const m = /^perm:(allow|deny|always|asn|asb|back
|
|
65149
|
+
const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
|
|
65162
65150
|
if (!m) {
|
|
65163
65151
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
65164
65152
|
return;
|
|
@@ -65178,9 +65166,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
65178
65166
|
}
|
|
65179
65167
|
let keyboard;
|
|
65180
65168
|
if (behavior === "back") {
|
|
65181
|
-
|
|
65182
|
-
const backTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(details.tool_name, details.input_preview, backChoices) != null;
|
|
65183
|
-
keyboard = buildPermissionActionRow(request_id, true, backTimeBox);
|
|
65169
|
+
keyboard = buildPermissionActionRow(request_id, true);
|
|
65184
65170
|
} else {
|
|
65185
65171
|
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
65186
65172
|
if (choices == null) {
|
|
@@ -65313,12 +65299,12 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
65313
65299
|
}
|
|
65314
65300
|
const ok = durable;
|
|
65315
65301
|
const legacyNote = legacy && durable;
|
|
65316
|
-
const
|
|
65302
|
+
const ackText2 = ok ? legacyNote ? `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking (legacy path).` : `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking.` : editLockHint ? `\u26A0\uFE0F Allowed for now \u2014 config edits are locked. Enable hostd.config_edit_enabled.` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
|
|
65317
65303
|
const sourceMsg = ctx.callbackQuery?.message;
|
|
65318
65304
|
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
65319
65305
|
const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
|
|
65320
65306
|
await finalizeCallback(ctx, {
|
|
65321
|
-
ackText:
|
|
65307
|
+
ackText: ackText2.slice(0, 200),
|
|
65322
65308
|
newText: baseText2 ? `${baseText2}
|
|
65323
65309
|
|
|
65324
65310
|
${editLabel}` : editLabel,
|
|
@@ -65326,64 +65312,28 @@ ${editLabel}` : editLabel,
|
|
|
65326
65312
|
});
|
|
65327
65313
|
return;
|
|
65328
65314
|
}
|
|
65329
|
-
|
|
65330
|
-
|
|
65331
|
-
|
|
65332
|
-
|
|
65333
|
-
|
|
65334
|
-
|
|
65335
|
-
|
|
65336
|
-
|
|
65337
|
-
|
|
65338
|
-
return;
|
|
65339
|
-
}
|
|
65340
|
-
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
65341
|
-
const tb = resolveTimeBox(details.tool_name, details.input_preview, choices);
|
|
65342
|
-
if (!tb) {
|
|
65343
|
-
await ctx.answerCallbackQuery({ text: "This action can't be time-boxed." }).catch(() => {});
|
|
65344
|
-
return;
|
|
65345
|
-
}
|
|
65346
|
-
const agentName3 = selfAgentName();
|
|
65347
|
-
if (!agentName3) {
|
|
65348
|
-
await ctx.answerCallbackQuery({ text: "Time-box needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
|
|
65349
|
-
return;
|
|
65350
|
-
}
|
|
65351
|
-
pendingPermissions.delete(request_id);
|
|
65352
|
-
dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior: "allow" });
|
|
65353
|
-
recordScopedGrant(scopedGrants, agentName3, tb.rule, Date.now(), ttl);
|
|
65354
|
-
resumeReactionAfterVerdict();
|
|
65355
|
-
postPermissionResumeMessage({
|
|
65356
|
-
behavior: "allow",
|
|
65357
|
-
action: naturalAction(details.tool_name, details.input_preview)
|
|
65358
|
-
});
|
|
65359
|
-
process.stderr.write(`telegram gateway: scoped-approval granted rule="${tb.rule}" agent=${agentName3} ttl_ms=${ttl} (request_id=${request_id})
|
|
65315
|
+
const pd = pendingPermissions.get(request_id);
|
|
65316
|
+
const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : "";
|
|
65317
|
+
const scopedTtl = scopedApprovalTtlMs();
|
|
65318
|
+
const timeBox = behavior === "allow" && scopedTtl > 0 && pd ? resolveTimeBox(pd.tool_name, pd.input_preview, resolveScopedAllowChoices(pd.tool_name, pd.input_preview)) : null;
|
|
65319
|
+
const grantAgent = selfAgentName();
|
|
65320
|
+
pendingPermissions.delete(request_id);
|
|
65321
|
+
if (timeBox && grantAgent) {
|
|
65322
|
+
recordScopedGrant(scopedGrants, grantAgent, timeBox.rule, Date.now(), scopedTtl);
|
|
65323
|
+
process.stderr.write(`telegram gateway: scoped-approval granted via Allow rule="${timeBox.rule}" agent=${grantAgent} ttl_ms=${scopedTtl} (request_id=${request_id})
|
|
65360
65324
|
`);
|
|
65361
|
-
const mins = Math.max(1, Math.round(ttl / 60000));
|
|
65362
|
-
const sourceMsg = ctx.callbackQuery?.message;
|
|
65363
|
-
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
65364
|
-
const editLabel = `\u23F1 <b>Allowed for ${mins} min \u2014 ${escapeHtmlForTg(tb.breadth)}</b> \xB7 re-asks after that, and now for anything else`;
|
|
65365
|
-
await finalizeCallback(ctx, {
|
|
65366
|
-
ackText: `\u23F1 Allowed for ${mins} min`.slice(0, 200),
|
|
65367
|
-
newText: baseText2 ? `${baseText2}
|
|
65368
|
-
|
|
65369
|
-
${editLabel}` : editLabel,
|
|
65370
|
-
parseMode: "HTML"
|
|
65371
|
-
});
|
|
65372
|
-
return;
|
|
65373
65325
|
}
|
|
65374
|
-
const
|
|
65375
|
-
|
|
65376
|
-
|
|
65377
|
-
|
|
65378
|
-
pendingPermissions.delete(request_id);
|
|
65379
|
-
const label = behavior === "allow" ? "\u2705 Allowed" : "\u274C Denied";
|
|
65326
|
+
const scopedMins = Math.max(1, Math.round(scopedTtl / 60000));
|
|
65327
|
+
const windowGranted = timeBox != null && grantAgent !== "";
|
|
65328
|
+
const ackText = behavior === "deny" ? "\u274C Denied" : windowGranted ? `\u2705 Allowed (${scopedMins} min)` : "\u2705 Allowed once";
|
|
65329
|
+
const htmlLabel = behavior === "deny" ? "\u274C <b>Denied</b>" : windowGranted ? `\u2705 <b>Allowed \u2014 won't ask again about ${escapeHtmlForTg(timeBox.breadth)} for ${scopedMins} min</b>` : "\u2705 <b>Allowed once</b>";
|
|
65380
65330
|
const msg = ctx.callbackQuery?.message;
|
|
65381
65331
|
const baseText = msg && "text" in msg && msg.text ? escapeHtmlForTg(msg.text) : "";
|
|
65382
65332
|
await finalizeCallback(ctx, {
|
|
65383
|
-
ackText:
|
|
65333
|
+
ackText: ackText.slice(0, 200),
|
|
65384
65334
|
newText: baseText ? `${baseText}
|
|
65385
65335
|
|
|
65386
|
-
${
|
|
65336
|
+
${htmlLabel}` : htmlLabel,
|
|
65387
65337
|
parseMode: "HTML",
|
|
65388
65338
|
synthInbound: () => {
|
|
65389
65339
|
dispatchPermissionVerdict({
|
|
@@ -3660,8 +3660,9 @@ function sweepStaleAlwaysAllowCorrelations(now = Date.now()): void {
|
|
|
3660
3660
|
}
|
|
3661
3661
|
}
|
|
3662
3662
|
|
|
3663
|
-
//
|
|
3664
|
-
//
|
|
3663
|
+
// Scoped-approval store: the 30-min window that backs the "✅ Allow" tap for
|
|
3664
|
+
// narrow non-destructive scopes (not a separate button — it IS what Allow
|
|
3665
|
+
// means for those). Operator-tapped, gateway-side ONLY (never pushed to the
|
|
3665
3666
|
// bridge's untimed sessionAllowRules), fixed-window, fail-closed. Keyed by
|
|
3666
3667
|
// agent name for per-agent isolation. All policy lives in
|
|
3667
3668
|
// ../scoped-approval.ts (pure + unit-tested); this gateway only wires it.
|
|
@@ -5605,24 +5606,27 @@ const pendingPermissionBuffer = createPendingPermissionBuffer()
|
|
|
5605
5606
|
* are resolved at call-time, after module init.)
|
|
5606
5607
|
*/
|
|
5607
5608
|
/**
|
|
5608
|
-
* The default permission-card action row: ❌ Deny · ✅ Allow
|
|
5609
|
+
* The default permission-card action row: ❌ Deny · ✅ Allow ·
|
|
5609
5610
|
* 🔁 Always… (the last only when a meaningful always-rule exists).
|
|
5610
5611
|
* Tapping "🔁 Always…" swaps this row for the scope sub-menu; "← Back"
|
|
5611
5612
|
* rebuilds this row. callback_data stays tiny (verb + 5-char id) so we
|
|
5612
5613
|
* never approach Telegram's 64-byte ceiling.
|
|
5614
|
+
*
|
|
5615
|
+
* "✅ Allow" is no longer always literally "once": for a NARROW,
|
|
5616
|
+
* non-destructive scope it auto-grants a fixed 30-min window (so the same
|
|
5617
|
+
* action stops re-asking) — that is the default behavior of the allow tap,
|
|
5618
|
+
* not a separate button. Broad / MCP / destructive scopes stay truly once.
|
|
5619
|
+
* The post-tap card states which happened (honest-card contract). The
|
|
5620
|
+
* decision lives in the allow handler via resolveTimeBox; the label here is
|
|
5621
|
+
* deliberately neutral ("Allow", not "Allow once" or "Allow 30 min").
|
|
5613
5622
|
*/
|
|
5614
5623
|
function buildPermissionActionRow(
|
|
5615
5624
|
requestId: string,
|
|
5616
5625
|
showAlways: boolean,
|
|
5617
|
-
showTimeBox = false,
|
|
5618
5626
|
): InlineKeyboard {
|
|
5619
5627
|
const kb = new InlineKeyboard()
|
|
5620
5628
|
.text('❌ Deny', `perm:deny:${requestId}`)
|
|
5621
|
-
.text('✅ Allow
|
|
5622
|
-
// "⏱ 30 min" sits between once and always. Only shown for a narrow,
|
|
5623
|
-
// non-destructive scope (resolveTimeBox decides); broad/MCP/destructive
|
|
5624
|
-
// requests get once/always only.
|
|
5625
|
-
if (showTimeBox) kb.text('⏱ 30 min', `perm:tmb:${requestId}`)
|
|
5629
|
+
.text('✅ Allow', `perm:allow:${requestId}`)
|
|
5626
5630
|
if (showAlways) kb.text('🔁 Always…', `perm:always:${requestId}`)
|
|
5627
5631
|
return kb
|
|
5628
5632
|
}
|
|
@@ -6044,19 +6048,15 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
6044
6048
|
description,
|
|
6045
6049
|
agentName: _client.agentName,
|
|
6046
6050
|
})
|
|
6047
|
-
// Compact action row: ❌ Deny · ✅ Allow
|
|
6048
|
-
//
|
|
6049
|
-
//
|
|
6050
|
-
//
|
|
6051
|
-
//
|
|
6052
|
-
//
|
|
6053
|
-
|
|
6054
|
-
const showAlways =
|
|
6055
|
-
|
|
6056
|
-
// when the tier is enabled (TTL > 0).
|
|
6057
|
-
const showTimeBox = scopedApprovalTtlMs() > 0 &&
|
|
6058
|
-
resolveTimeBox(toolName, inputPreview, scopeChoices) != null
|
|
6059
|
-
const keyboard = buildPermissionActionRow(requestId, showAlways, showTimeBox)
|
|
6051
|
+
// Compact action row: ❌ Deny · ✅ Allow · 🔁 Always… — the scope of an
|
|
6052
|
+
// "always" grant stays hidden until the operator taps "🔁 Always…",
|
|
6053
|
+
// which swaps the row for a scope choice (this file / any file ⚠️). The
|
|
6054
|
+
// "🔁 Always…" button only appears when we can synthesize a meaningful
|
|
6055
|
+
// rule for this tool; unknown tools get the two-button row only. "Allow"
|
|
6056
|
+
// itself auto-grants a 30-min window for narrow non-destructive scopes
|
|
6057
|
+
// (decided in the allow handler), so there is no separate time-box button.
|
|
6058
|
+
const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null
|
|
6059
|
+
const keyboard = buildPermissionActionRow(requestId, showAlways)
|
|
6060
6060
|
// Route the card to the SAME place the post-verdict resume message
|
|
6061
6061
|
// lands (resolvePermissionCardTargets): the ORIGINATING chat+topic when
|
|
6062
6062
|
// there's an active turn — so a supergroup agent's card appears IN the
|
|
@@ -19073,7 +19073,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
19073
19073
|
}
|
|
19074
19074
|
|
|
19075
19075
|
// Permission request buttons.
|
|
19076
|
-
const m = /^perm:(allow|deny|always|asn|asb|back
|
|
19076
|
+
const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data)
|
|
19077
19077
|
if (!m) { await ctx.answerCallbackQuery().catch(() => {}); return }
|
|
19078
19078
|
const access = loadAccess()
|
|
19079
19079
|
const senderId = String(ctx.from.id)
|
|
@@ -19088,10 +19088,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
19088
19088
|
if (!details) { await ctx.answerCallbackQuery({ text: 'Details no longer available.' }).catch(() => {}); return }
|
|
19089
19089
|
let keyboard: InlineKeyboard
|
|
19090
19090
|
if (behavior === 'back') {
|
|
19091
|
-
|
|
19092
|
-
const backTimeBox = scopedApprovalTtlMs() > 0 &&
|
|
19093
|
-
resolveTimeBox(details.tool_name, details.input_preview, backChoices) != null
|
|
19094
|
-
keyboard = buildPermissionActionRow(request_id, true, backTimeBox)
|
|
19091
|
+
keyboard = buildPermissionActionRow(request_id, true)
|
|
19095
19092
|
} else {
|
|
19096
19093
|
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview)
|
|
19097
19094
|
if (choices == null) {
|
|
@@ -19315,69 +19312,47 @@ bot.on('callback_query:data', async ctx => {
|
|
|
19315
19312
|
return
|
|
19316
19313
|
}
|
|
19317
19314
|
|
|
19318
|
-
//
|
|
19319
|
-
//
|
|
19320
|
-
//
|
|
19321
|
-
//
|
|
19322
|
-
//
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
// strictly gateway-side; the bridge must not cache it untimed).
|
|
19337
|
-
dispatchPermissionVerdict({ type: 'permission', requestId: request_id, behavior: 'allow' })
|
|
19338
|
-
// (2) Record the fixed-window grant so matching calls auto-allow.
|
|
19339
|
-
recordScopedGrant(scopedGrants, agentName, tb.rule, Date.now(), ttl)
|
|
19340
|
-
resumeReactionAfterVerdict()
|
|
19341
|
-
postPermissionResumeMessage({
|
|
19342
|
-
behavior: 'allow',
|
|
19343
|
-
action: naturalAction(details.tool_name, details.input_preview),
|
|
19344
|
-
})
|
|
19315
|
+
// Forward permission decision to connected bridges. Capture the pending
|
|
19316
|
+
// details BEFORE deleting the entry — the resume message names the resumed
|
|
19317
|
+
// work, and "Allow" on a narrow non-destructive scope auto-grants a 30-min
|
|
19318
|
+
// window (resolveTimeBox) so the same action stops re-asking. This IS the
|
|
19319
|
+
// default behavior of Allow (no separate button); broad / MCP / destructive
|
|
19320
|
+
// scopes (resolveTimeBox → null) and the disabled tier (ttl<=0) stay truly
|
|
19321
|
+
// once. The verdict is still dispatched WITHOUT a `rule` (below), so the
|
|
19322
|
+
// bridge never caches it untimed — the window lives only in scopedGrants.
|
|
19323
|
+
const pd = pendingPermissions.get(request_id)
|
|
19324
|
+
const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : ''
|
|
19325
|
+
const scopedTtl = scopedApprovalTtlMs()
|
|
19326
|
+
const timeBox = (behavior === 'allow' && scopedTtl > 0 && pd)
|
|
19327
|
+
? resolveTimeBox(pd.tool_name, pd.input_preview, resolveScopedAllowChoices(pd.tool_name, pd.input_preview))
|
|
19328
|
+
: null
|
|
19329
|
+
const grantAgent = selfAgentName()
|
|
19330
|
+
pendingPermissions.delete(request_id)
|
|
19331
|
+
if (timeBox && grantAgent) {
|
|
19332
|
+
recordScopedGrant(scopedGrants, grantAgent, timeBox.rule, Date.now(), scopedTtl)
|
|
19345
19333
|
process.stderr.write(
|
|
19346
|
-
`telegram gateway: scoped-approval granted rule="${
|
|
19347
|
-
`ttl_ms=${
|
|
19334
|
+
`telegram gateway: scoped-approval granted via Allow rule="${timeBox.rule}" ` +
|
|
19335
|
+
`agent=${grantAgent} ttl_ms=${scopedTtl} (request_id=${request_id})\n`,
|
|
19348
19336
|
)
|
|
19349
|
-
|
|
19350
|
-
const mins = Math.max(1, Math.round(ttl / 60_000))
|
|
19351
|
-
// Honest card: state the real BREADTH (e.g. "any `git` command"), not
|
|
19352
|
-
// just the rule, plus the window — consent covers both (access-model
|
|
19353
|
-
// honest-card contract).
|
|
19354
|
-
const sourceMsg = ctx.callbackQuery?.message
|
|
19355
|
-
const baseText = sourceMsg && 'text' in sourceMsg && sourceMsg.text
|
|
19356
|
-
? escapeHtmlForTg(sourceMsg.text)
|
|
19357
|
-
: ''
|
|
19358
|
-
const editLabel = `⏱ <b>Allowed for ${mins} min — ${escapeHtmlForTg(tb.breadth)}</b> · re-asks after that, and now for anything else`
|
|
19359
|
-
await finalizeCallback(ctx, {
|
|
19360
|
-
ackText: `⏱ Allowed for ${mins} min`.slice(0, 200),
|
|
19361
|
-
newText: baseText ? `${baseText}\n\n${editLabel}` : editLabel,
|
|
19362
|
-
parseMode: 'HTML',
|
|
19363
|
-
})
|
|
19364
|
-
return
|
|
19365
19337
|
}
|
|
19366
|
-
|
|
19367
|
-
// Forward permission decision to connected bridges. Capture the work
|
|
19368
|
-
// phrase BEFORE deleting the pending entry — postPermissionResumeMessage
|
|
19369
|
-
// (fired in synthInbound below) names the resumed work.
|
|
19370
|
-
const resumeAction = (() => {
|
|
19371
|
-
const d = pendingPermissions.get(request_id)
|
|
19372
|
-
return d ? naturalAction(d.tool_name, d.input_preview) : ''
|
|
19373
|
-
})()
|
|
19374
|
-
pendingPermissions.delete(request_id)
|
|
19338
|
+
const scopedMins = Math.max(1, Math.round(scopedTtl / 60_000))
|
|
19375
19339
|
// The card collapses to a plain verdict label. The distinct agent-voiced
|
|
19376
19340
|
// "got it, continuing: …" message (posted on resume below) now carries
|
|
19377
19341
|
// the "is it working or did my tap do nothing?" signal the old
|
|
19378
19342
|
// `▶️ resuming…` card footnote used to — and names the work, which the
|
|
19379
19343
|
// footnote never did. Keeps the card terse and the resume legible.
|
|
19380
|
-
|
|
19344
|
+
// Honest-card: only claim the window when one was actually recorded
|
|
19345
|
+
// (timeBox eligible AND we had an agent name to key it under) — never
|
|
19346
|
+
// promise "won't ask again for 30 min" if nothing was stored.
|
|
19347
|
+
const windowGranted = timeBox != null && grantAgent !== ''
|
|
19348
|
+
const ackText = behavior === 'deny'
|
|
19349
|
+
? '❌ Denied'
|
|
19350
|
+
: (windowGranted ? `✅ Allowed (${scopedMins} min)` : '✅ Allowed once')
|
|
19351
|
+
const htmlLabel = behavior === 'deny'
|
|
19352
|
+
? '❌ <b>Denied</b>'
|
|
19353
|
+
: (windowGranted
|
|
19354
|
+
? `✅ <b>Allowed — won't ask again about ${escapeHtmlForTg(timeBox!.breadth)} for ${scopedMins} min</b>`
|
|
19355
|
+
: '✅ <b>Allowed once</b>')
|
|
19381
19356
|
// HTML-escape the source text — same hazard as the scope-commit and
|
|
19382
19357
|
// recent-denial paths above. The permission card body
|
|
19383
19358
|
// (formatPermissionCardBody) appends claude-supplied `description`
|
|
@@ -19396,10 +19371,12 @@ bot.on('callback_query:data', async ctx => {
|
|
|
19396
19371
|
// permission was already broadcast. Routing through finalizeCallback
|
|
19397
19372
|
// strips the keyboard atomically with the status-line edit.
|
|
19398
19373
|
await finalizeCallback(ctx, {
|
|
19399
|
-
ackText:
|
|
19400
|
-
newText: baseText ? `${baseText}\n\n${
|
|
19374
|
+
ackText: ackText.slice(0, 200),
|
|
19375
|
+
newText: baseText ? `${baseText}\n\n${htmlLabel}` : htmlLabel,
|
|
19401
19376
|
parseMode: 'HTML',
|
|
19402
19377
|
synthInbound: () => {
|
|
19378
|
+
// No `rule` → the bridge does NOT cache this (truly once on the bridge);
|
|
19379
|
+
// any 30-min stickiness lives only in scopedGrants (recorded above).
|
|
19403
19380
|
dispatchPermissionVerdict({
|
|
19404
19381
|
type: 'permission',
|
|
19405
19382
|
requestId: request_id,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scoped, time-boxed approval — the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Scoped, time-boxed approval — the default behavior of the "✅ Allow" tap
|
|
3
|
+
* for a NARROW, non-destructive scope. Tapping Allow on such a request
|
|
4
|
+
* auto-grants a fixed window so byte-identical in-scope requests auto-allow
|
|
5
|
+
* without re-carding (killing tap-fatigue on re-edits / re-runs). Broad /
|
|
6
|
+
* MCP / destructive scopes get no window — Allow stays truly once for them.
|
|
7
|
+
* "🔁 Always…" remains the separate durable (`tools.allow`, forever) tier.
|
|
8
|
+
* There is deliberately no separate time-box button: the window IS what
|
|
9
|
+
* "Allow" means for a narrow safe scope, disclosed honestly on the post-tap
|
|
10
|
+
* card ("won't ask again about <breadth> for 30 min" vs "allowed once").
|
|
7
11
|
*
|
|
8
12
|
* Design contract (reference/access-model.md — "you hold the leash"):
|
|
9
13
|
*
|
|
@@ -21,14 +25,13 @@
|
|
|
21
25
|
* - **Conservative scope (this tier, v1).** Only the *narrow* scope is
|
|
22
26
|
* ever time-boxed: an exact file path (`Edit(/x.ts)`) or a Bash
|
|
23
27
|
* command-family (`Bash(git:*)`). Broad scopes ("any file", resource-
|
|
24
|
-
* blind MCP, "any command")
|
|
25
|
-
* once / always. This covers the real fatigue (re-editing the same
|
|
28
|
+
* blind MCP, "any command") get NO window — Allow stays truly once for them. This covers the real fatigue (re-editing the same
|
|
26
29
|
* file, re-running a safe command) without fanning one tap across an
|
|
27
30
|
* unbounded action set.
|
|
28
31
|
* - **Fail-closed on irreversible.** A Bash family grant (`Bash(git:*)`)
|
|
29
32
|
* must never auto-allow a destructive member of that family
|
|
30
33
|
* (`git push --force`, `git reset --hard`). `isDestructiveBashCommand`
|
|
31
|
-
* is re-checked at BOTH grant time (
|
|
34
|
+
* is re-checked at BOTH grant time (no window granted) and match time
|
|
32
35
|
* (a cached family grant fails closed → re-cards) so per-call consent
|
|
33
36
|
* for irreversible actions is preserved.
|
|
34
37
|
*
|
|
@@ -45,8 +48,8 @@ export const SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Resolve the configured window from the environment. `0` (or negative)
|
|
48
|
-
* disables the
|
|
49
|
-
* short-circuits. A blank/garbage value falls back to the 30-min default.
|
|
51
|
+
* disables the window — Allow becomes truly once for every scope and the
|
|
52
|
+
* gateway never short-circuits. A blank/garbage value falls back to the 30-min default.
|
|
50
53
|
* Kill-switch: `SWITCHROOM_SCOPED_APPROVAL_TTL_MS=0`.
|
|
51
54
|
*/
|
|
52
55
|
export function scopedApprovalTtlMs(
|
|
@@ -84,7 +87,7 @@ const BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
|
|
|
84
87
|
* Conservative time-box eligibility. Given the already-resolved scope
|
|
85
88
|
* choices for a permission request, return the NARROW rule to time-box
|
|
86
89
|
* plus an honest breadth phrase — or `null` when this request must not
|
|
87
|
-
* get a
|
|
90
|
+
* get a window (broad-only tools, MCP, Skill, a destructive Bash
|
|
88
91
|
* command, or any tool with no narrow sub-scope).
|
|
89
92
|
*
|
|
90
93
|
* - File tools with an exact path → time-boxable (bounded to the one
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the
|
|
2
|
+
* Tests for scoped-approval.ts — the 30-min window backing the "Allow" tap
|
|
3
3
|
* the middle rung between "Allow once" and "🔁 Always".
|
|
4
4
|
*
|
|
5
5
|
* These pin the access-model invariants the adversarial review flagged as
|
|
@@ -233,7 +233,7 @@ describe('isDestructiveBashCommand — fail-closed denylist', () => {
|
|
|
233
233
|
recordScopedGrant(store, 'clerk', 'Bash(git:*)', T0, TTL)
|
|
234
234
|
// first token is the harmless `git`, but the backtick hides `rm -rf`
|
|
235
235
|
expect(lookupScopedGrant(store, 'clerk', 'Bash', bashInput('git status `rm -rf /important`'), T0 + 1)).toBeNull()
|
|
236
|
-
// and the request never gets
|
|
236
|
+
// and the request never gets a window at grant time either
|
|
237
237
|
expect(timeBoxRule('Bash', bashInput('git status `rm -rf x`'))).toBeNull()
|
|
238
238
|
})
|
|
239
239
|
|