baychat 0.35.4 → 0.37.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/approve-hook.js +18 -4
- package/dist/channel/channel-check.js +53 -0
- package/dist/channel/claude-link.js +9 -4
- package/dist/channel/feed.js +59 -5
- package/dist/channel/server.js +60 -12
- package/dist/denied/claims.js +75 -0
- package/dist/denied/compound-command.js +51 -0
- package/dist/denied/opt-in.js +30 -16
- package/dist/denied/permission-rule.js +10 -9
- package/dist/denied-hook.js +26 -1
- package/dist/launch/command.js +15 -4
- package/dist/launch/policy.js +12 -2
- package/dist/main.js +4 -0
- package/dist/mcp.js +3 -0
- package/dist/permissions-command.js +8 -5
- package/dist/protocol-content.js +1 -1
- package/dist/relay/launcher/approval-settings.js +2 -2
- package/dist/relay/launcher/handle.js +41 -0
- package/dist/relay/launcher/loop.js +9 -2
- package/dist/runtimes.js +4 -0
- package/dist/tool-defs.js +20 -3
- package/hooks/hooks.json +30 -0
- package/package.json +2 -1
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
// The card text is built from `shown`, which masks obvious secrets; the rule is built from the
|
|
23
23
|
// real text, because a rule that differs from the call matches nothing.
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.MAX_RULE_COMMAND_LENGTH = void 0;
|
|
25
|
+
exports.CHAINED_LIMITATION = exports.MAX_RULE_COMMAND_LENGTH = void 0;
|
|
26
26
|
exports.planDeniedCall = planDeniedCall;
|
|
27
27
|
exports.callKeyOf = callKeyOf;
|
|
28
28
|
exports.sameCall = sameCall;
|
|
29
29
|
exports.redactSecrets = redactSecrets;
|
|
30
|
+
const compound_command_1 = require("./compound-command");
|
|
30
31
|
/** Longer than this is not a command a person can check on a phone, nor one a rule should hold. */
|
|
31
32
|
exports.MAX_RULE_COMMAND_LENGTH = 1000;
|
|
32
33
|
/** Claude Code itself saves at most five rules for one approved chain (permissions docs). */
|
|
33
34
|
const MAX_CHAIN_PARTS = 5;
|
|
35
|
+
/** What the card says for every chain: the agent reads it too (the deny message, approve-hook). */
|
|
36
|
+
exports.CHAINED_LIMITATION = `${compound_command_1.CHAINED_LINE} Ask the session to split it into single commands or a script it runs with one command.`;
|
|
34
37
|
/** `&&`, `||`, `|&`, `;`, `|`, and a lone `&` that is not part of a redirection (`2>&1`, `&>`). */
|
|
35
|
-
const CHAIN_OPERATOR = /&&|\|\||\|&|;|\||(?<![>&])&(?![>&])/;
|
|
36
38
|
const CHAIN_SPLIT = /\s*(?:&&|\|\||\|&|;|\||(?<![>&])&(?![>&]))\s*/;
|
|
37
39
|
const NESTED_COMMAND = /`|\$\(|[()]/;
|
|
38
40
|
const MCP_TOOL = /^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_.-]+$/;
|
|
@@ -75,23 +77,22 @@ function planBash(command) {
|
|
|
75
77
|
"approve it at the computer.");
|
|
76
78
|
}
|
|
77
79
|
const call = { tool: "Bash", subject: command };
|
|
78
|
-
|
|
80
|
+
// A chain as the shell reads it (compound-command.ts): `echo "a && b"` is one command.
|
|
81
|
+
if ((0, compound_command_1.compoundKind)(command) === "chain") {
|
|
79
82
|
if (/["'\\]/.test(command)) {
|
|
80
|
-
return nothing(shown,
|
|
81
|
-
"run them one at a time.");
|
|
83
|
+
return nothing(shown, `${exports.CHAINED_LIMITATION} It also quotes text, so BayChat cannot split it safely; approve it at the computer.`);
|
|
82
84
|
}
|
|
83
85
|
const parts = command.split(CHAIN_SPLIT);
|
|
84
86
|
if (parts.some((p) => p === "") || parts.length > MAX_CHAIN_PARTS) {
|
|
85
|
-
return nothing(shown,
|
|
87
|
+
return nothing(shown, `${exports.CHAINED_LIMITATION} BayChat cannot split this one; approve it at the computer.`);
|
|
86
88
|
}
|
|
87
89
|
return {
|
|
88
90
|
shown,
|
|
89
91
|
onceRules: [...new Set(parts.map((p) => `Bash(${p})`))],
|
|
90
92
|
alwaysAllowed: false,
|
|
91
93
|
// One plain line on WHY, because the usual cause is the agent's own habit (`cd … && x | tail`)
|
|
92
|
-
// and the fix is for the session to run
|
|
93
|
-
limitation:
|
|
94
|
-
`command. Run it on its own (no cd … &&, no | tail) to be offered "Always".`,
|
|
94
|
+
// and the fix is for the session to run single commands.
|
|
95
|
+
limitation: exports.CHAINED_LIMITATION,
|
|
95
96
|
call,
|
|
96
97
|
};
|
|
97
98
|
}
|
package/dist/denied-hook.js
CHANGED
|
@@ -88,11 +88,13 @@ const approve_hook_1 = require("./approve-hook");
|
|
|
88
88
|
const approval_wait_1 = require("./approval-wait");
|
|
89
89
|
const config_1 = require("./config");
|
|
90
90
|
const hook_stdin_1 = require("./hook-stdin");
|
|
91
|
+
const claims_1 = require("./denied/claims");
|
|
91
92
|
const confirm_1 = require("./denied/confirm");
|
|
92
93
|
const grants_1 = require("./denied/grants");
|
|
93
94
|
const hook_log_1 = require("./denied/hook-log");
|
|
94
95
|
const live_session_1 = require("./denied/live-session");
|
|
95
96
|
const notice_1 = require("./denied/notice");
|
|
97
|
+
const opt_in_1 = require("./denied/opt-in");
|
|
96
98
|
const permission_rule_1 = require("./denied/permission-rule");
|
|
97
99
|
const settings_rules_1 = require("./denied/settings-rules");
|
|
98
100
|
const transcript_1 = require("./denied/transcript");
|
|
@@ -180,6 +182,7 @@ function parseArgs(argv) {
|
|
|
180
182
|
return {
|
|
181
183
|
session: at("--session"),
|
|
182
184
|
fromClaude: argv.includes("--session-from-claude"),
|
|
185
|
+
plugin: argv.includes("--plugin"),
|
|
183
186
|
waitSeconds: Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : approve_hook_1.DEFAULT_WAIT_SECONDS,
|
|
184
187
|
};
|
|
185
188
|
}
|
|
@@ -227,6 +230,22 @@ async function decideDenied(rawStdin, argv = [], deps = {}) {
|
|
|
227
230
|
log(`${LOG_LABEL}: this session has not joined BayChat yet; the denial stands.`);
|
|
228
231
|
return null;
|
|
229
232
|
}
|
|
233
|
+
// The switch decides on every denial: the plugin's hook runs in every session, and a launched
|
|
234
|
+
// session's own hook was written while the switch was not off (it may have changed since).
|
|
235
|
+
const mode = (deps.cardMode ?? opt_in_1.deniedCardMode)();
|
|
236
|
+
if (mode === "off") {
|
|
237
|
+
log(`${LOG_LABEL}: the denied-command card is off on this computer; the denial stands.`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const eventId = str(input.tool_use_id);
|
|
241
|
+
if (opts.plugin && !eventId) {
|
|
242
|
+
log(`${LOG_LABEL}: the event has no tool_use_id to take it by; the denial stands.`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (eventId && !(deps.claimEvent ?? claims_1.claimDeniedEvent)(eventId)) {
|
|
246
|
+
log(`${LOG_LABEL}: another BayChat hook already took this denial.`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
230
249
|
const device = (deps.loadDevice ?? config_1.loadDeviceCredentials)();
|
|
231
250
|
if (!device) {
|
|
232
251
|
log(`${LOG_LABEL}: this machine is not logged in to BayChat (\`baychat login\`); the denial stands.`);
|
|
@@ -301,7 +320,13 @@ async function decideDenied(rawStdin, argv = [], deps = {}) {
|
|
|
301
320
|
limitation: `BayChat can't allow it from here: ${choice.why}, so no rule can be saved. ` +
|
|
302
321
|
"Approve it at the computer (/permissions → Recently denied).",
|
|
303
322
|
}
|
|
304
|
-
: planned
|
|
323
|
+
: mode === "once" && planned.alwaysAllowed
|
|
324
|
+
? {
|
|
325
|
+
...planned,
|
|
326
|
+
alwaysAllowed: false,
|
|
327
|
+
limitation: "\"Always\" is off on this computer (`baychat permissions on` at the computer turns it on).",
|
|
328
|
+
}
|
|
329
|
+
: planned;
|
|
305
330
|
const target = choice.scope === "none" ? undefined : choice;
|
|
306
331
|
const question = composeDeniedQuestion({
|
|
307
332
|
who,
|
package/dist/launch/command.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.trustByHand = exports.CODEX_UNATTENDED_NOTICE = exports.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
|
|
36
|
+
exports.trustByHand = exports.CODEX_UNATTENDED_NOTICE = exports.AGENT_STARTS_NOTICE = exports.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
|
|
37
37
|
exports.cmdLaunch = cmdLaunch;
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const args_1 = require("../args");
|
|
@@ -41,13 +41,18 @@ const multiplexer_launch_1 = require("../multiplexer-launch");
|
|
|
41
41
|
const policy_1 = require("../relay/acp/policy");
|
|
42
42
|
const claude_trust_1 = require("./claude-trust");
|
|
43
43
|
const policy_2 = require("./policy");
|
|
44
|
-
exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended]
|
|
44
|
+
exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended] [--agent-starts]
|
|
45
45
|
Let BayChat start sessions in <folder> from the app
|
|
46
46
|
(--unattended: also let it start Codex there, which then never asks)
|
|
47
|
+
(--agent-starts: agents you allowed "Always" may start sessions there without asking you each time)
|
|
47
48
|
baychat launch list The folders BayChat may start sessions in
|
|
48
49
|
baychat launch remove <id> Stop offering one of them`;
|
|
49
50
|
/** Printed by every `add`: the one thing a person must know before saying yes. */
|
|
50
51
|
exports.APPROVALS_NOTICE = "Claude Code sessions started here from BayChat ask for approvals in BayChat.";
|
|
52
|
+
/** Printed by an `add … --agent-starts`: what the second key opens, and what it never does. */
|
|
53
|
+
exports.AGENT_STARTS_NOTICE = "Agents you allowed with \"Always for this agent here\" on their card may start sessions here without asking you each time — " +
|
|
54
|
+
"only for this folder, computer and runtime, for 30 days, and only with no task or one of your own messages as the task: a task an agent or anyone else writes still asks. " +
|
|
55
|
+
"Those sessions ask for approvals in BayChat like any other; they never start Codex unattended. Revoke an agent in Settings → Devices.";
|
|
51
56
|
/** Printed when Codex is offered: what it may do without asking, and what it asks on the phone. */
|
|
52
57
|
exports.CODEX_UNATTENDED_NOTICE = "Codex started here from BayChat is run by the relay (Codex 0.160 or newer): it can change files in this folder without asking, with no network, " +
|
|
53
58
|
"and asks for approval in BayChat — on the session owner's phone — before anything more (a command outside its sandbox, network access, files elsewhere). " +
|
|
@@ -89,6 +94,7 @@ async function addSpot(args, deps) {
|
|
|
89
94
|
const runtimeList = (0, args_1.flag)(args, "--runtime");
|
|
90
95
|
const maxRaw = (0, args_1.flag)(args, "--max");
|
|
91
96
|
const unattended = args.includes("--unattended");
|
|
97
|
+
const agentStarts = args.includes("--agent-starts");
|
|
92
98
|
const valued = new Set([label, runtimeList, maxRaw].filter((v) => v !== undefined));
|
|
93
99
|
const folderArg = args.find((a) => !a.startsWith("--") && !valued.has(a));
|
|
94
100
|
if (unattended && !(runtimeList ?? "").split(",").map((r) => r.trim()).includes("codex")) {
|
|
@@ -127,11 +133,14 @@ async function addSpot(args, deps) {
|
|
|
127
133
|
runtimes: [...new Set(requested)],
|
|
128
134
|
maxSessions,
|
|
129
135
|
...(unattended ? { unattendedCodex: true } : {}),
|
|
136
|
+
...(agentStarts ? { agentStarts: true } : {}),
|
|
130
137
|
};
|
|
131
138
|
(0, policy_2.saveLaunchPolicy)((0, policy_2.upsertSpot)(policy, spot), deps.policyFile);
|
|
132
139
|
deps.print(`${existing ? "Updated" : "Added"} "${spot.label}" (${spot.id}): ${spot.path}`);
|
|
133
140
|
deps.print(` Agents: ${spot.runtimes.join(", ")} · at most ${spot.maxSessions} at once`);
|
|
134
141
|
deps.print(` ${exports.APPROVALS_NOTICE}`);
|
|
142
|
+
if (spot.agentStarts)
|
|
143
|
+
deps.print(` ${exports.AGENT_STARTS_NOTICE}`);
|
|
135
144
|
if (spot.runtimes.includes("codex")) {
|
|
136
145
|
deps.print(spot.unattendedCodex
|
|
137
146
|
? ` ${exports.CODEX_UNATTENDED_NOTICE}`
|
|
@@ -176,8 +185,10 @@ function listSpots(deps) {
|
|
|
176
185
|
deps.print("BayChat may not start sessions on this computer. Add a folder with `baychat launch add <folder>`.");
|
|
177
186
|
return 0;
|
|
178
187
|
}
|
|
179
|
-
for (const s of policy.spots)
|
|
180
|
-
|
|
188
|
+
for (const s of policy.spots) {
|
|
189
|
+
const flags = [s.unattendedCodex ? "unattended Codex" : "", s.agentStarts ? "agent starts" : ""].filter(Boolean);
|
|
190
|
+
deps.print(`${s.id} "${s.label}" ${s.path} [${s.runtimes.join(", ")}] max ${s.maxSessions}${flags.length ? ` (${flags.join(", ")})` : ""}`);
|
|
191
|
+
}
|
|
181
192
|
return 0;
|
|
182
193
|
}
|
|
183
194
|
function removeSpotById(args, deps) {
|
package/dist/launch/policy.js
CHANGED
|
@@ -136,7 +136,7 @@ function resolveSpotFolder(spot) {
|
|
|
136
136
|
function parseSpot(entry) {
|
|
137
137
|
if (!entry || typeof entry !== "object")
|
|
138
138
|
return null;
|
|
139
|
-
const { id, label, path: p, runtimes, maxSessions, unattendedCodex } = entry;
|
|
139
|
+
const { id, label, path: p, runtimes, maxSessions, unattendedCodex, agentStarts } = entry;
|
|
140
140
|
if (typeof id !== "string" || !exports.SPOT_ID_RE.test(id))
|
|
141
141
|
return null;
|
|
142
142
|
if (typeof label !== "string" || label.trim().length === 0 || label.length > exports.SPOT_LABEL_MAX)
|
|
@@ -150,5 +150,15 @@ function parseSpot(entry) {
|
|
|
150
150
|
return null;
|
|
151
151
|
if (unattendedCodex !== undefined && unattendedCodex !== true)
|
|
152
152
|
return null;
|
|
153
|
-
|
|
153
|
+
if (agentStarts !== undefined && agentStarts !== true)
|
|
154
|
+
return null;
|
|
155
|
+
return {
|
|
156
|
+
id,
|
|
157
|
+
label,
|
|
158
|
+
path: p,
|
|
159
|
+
runtimes: [...new Set(runtimes)],
|
|
160
|
+
maxSessions,
|
|
161
|
+
...(unattendedCodex ? { unattendedCodex } : {}),
|
|
162
|
+
...(agentStarts ? { agentStarts } : {}),
|
|
163
|
+
};
|
|
154
164
|
}
|
package/dist/main.js
CHANGED
|
@@ -6,6 +6,7 @@ const approve_hook_1 = require("./approve-hook");
|
|
|
6
6
|
const denied_hook_1 = require("./denied-hook");
|
|
7
7
|
const permissions_command_1 = require("./permissions-command");
|
|
8
8
|
const context_hooks_command_1 = require("./context-hooks-command");
|
|
9
|
+
const channel_check_1 = require("./channel/channel-check");
|
|
9
10
|
const session_name_1 = require("./session-name");
|
|
10
11
|
const start_command_1 = require("./start-command");
|
|
11
12
|
const update_command_1 = require("./update-command");
|
|
@@ -337,6 +338,9 @@ async function main() {
|
|
|
337
338
|
return await (0, permissions_command_1.cmdPermissions)(args);
|
|
338
339
|
case "context-hooks":
|
|
339
340
|
return await (0, context_hooks_command_1.cmdContextHooks)(args);
|
|
341
|
+
case "channel-check":
|
|
342
|
+
// The plugin's SessionStart (compact) hook. Never throws, always 0 (channel/channel-check.ts).
|
|
343
|
+
return await (0, channel_check_1.cmdChannelCheck)();
|
|
340
344
|
case "doctor":
|
|
341
345
|
// Deliberately placed with the setup commands: it is the thing you run
|
|
342
346
|
// when one of them did not take, and the one command that inspects all
|
package/dist/mcp.js
CHANGED
|
@@ -252,6 +252,9 @@ async function handleSendMessage(args) {
|
|
|
252
252
|
// message that quotes nothing must not claim to quote something.
|
|
253
253
|
...(args.replyToMessageId ? { replyToMessageId: args.replyToMessageId } : {}),
|
|
254
254
|
...(args.card ? { card: args.card } : {}),
|
|
255
|
+
...(args.threadRootId ? { threadRootId: args.threadRootId } : {}),
|
|
256
|
+
...(args.handoff !== undefined ? { handoff: args.handoff } : {}),
|
|
257
|
+
...(args.result !== undefined ? { result: args.result } : {}),
|
|
255
258
|
});
|
|
256
259
|
const carried = ids.length > 0 ? ` with ${ids.length} attachment${ids.length === 1 ? "" : "s"}` : "";
|
|
257
260
|
return (0, mcp_result_1.ok)(`Sent message ${message.id} at ${message.createdAt}${carried}.`, message);
|
|
@@ -26,9 +26,12 @@ async function cmdPermissions(args, deps = {}) {
|
|
|
26
26
|
const [sub = "list", ...rest] = args;
|
|
27
27
|
switch (sub) {
|
|
28
28
|
case "list": {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const mode = (0, opt_in_1.deniedCardMode)();
|
|
30
|
+
out(mode === "on"
|
|
31
|
+
? "The denied-command card is ON, with Always, for sessions joined to BayChat (`baychat permissions off` turns it off)."
|
|
32
|
+
: mode === "once"
|
|
33
|
+
? "The denied-command card offers Allow once for sessions joined to BayChat (`baychat permissions on` adds Always, `off` turns it off)."
|
|
34
|
+
: "The denied-command card is OFF (`baychat permissions on` turns it on).");
|
|
32
35
|
const grants = (0, grants_1.listGrants)();
|
|
33
36
|
if (grants.length === 0) {
|
|
34
37
|
out("BayChat keeps no permission rules on this computer.");
|
|
@@ -60,13 +63,13 @@ async function cmdPermissions(args, deps = {}) {
|
|
|
60
63
|
return 1;
|
|
61
64
|
}
|
|
62
65
|
(0, opt_in_1.enableDeniedCard)();
|
|
63
|
-
out("The denied-command card is ON
|
|
66
|
+
out("The denied-command card is ON, with Always. A session joined to BayChat puts a card in BayChat when");
|
|
64
67
|
out("Claude Code's auto mode blocks a call. Read docs/features/DENIED_COMMAND_CARD.md for what it can and can't allow.");
|
|
65
68
|
return 0;
|
|
66
69
|
}
|
|
67
70
|
case "off":
|
|
68
71
|
(0, opt_in_1.disableDeniedCard)();
|
|
69
|
-
out("The denied-command card is OFF
|
|
72
|
+
out("The denied-command card is OFF. Rules it kept stay until removed.");
|
|
70
73
|
return 0;
|
|
71
74
|
case "sweep": {
|
|
72
75
|
const now = deps.now ?? Date.now;
|