rulesync 31.0.0 → 32.0.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/dist/cli/index.cjs +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CBvXTNp8.cjs → import-Bmb7ofrF.cjs} +253 -48
- package/dist/{import-ORYbMDJK.js → import-DgKWnDni.js} +237 -32
- package/dist/{import-ORYbMDJK.js.map → import-DgKWnDni.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -453,36 +453,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
453
453
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
454
454
|
//#endregion
|
|
455
455
|
//#region src/constants/rulesync-paths.ts
|
|
456
|
-
const { join: join$
|
|
456
|
+
const { join: join$417 } = node_path.posix;
|
|
457
457
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
458
458
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
459
459
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
460
460
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
461
461
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
462
462
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
463
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
463
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$417(RULES_FEATURE_SUBDIR, ".curated");
|
|
464
464
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
465
465
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
466
466
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
467
467
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
468
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
469
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
470
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
471
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
472
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
473
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
474
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
475
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
476
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
477
|
-
join$
|
|
478
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
479
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
468
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$417(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
469
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
470
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
471
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
472
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
473
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
475
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
476
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
477
|
+
join$417(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
478
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
479
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
480
480
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
481
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
481
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
482
482
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
483
483
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
484
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
485
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
484
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
485
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$417(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
486
486
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
487
487
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
488
488
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -4845,13 +4845,11 @@ const PI_HOOK_EVENTS = [
|
|
|
4845
4845
|
/**
|
|
4846
4846
|
* Hook events supported by oh-my-pi (`omp`), Pi's fork, through the same
|
|
4847
4847
|
* generated TypeScript extension. oh-my-pi keeps Pi's names for every event
|
|
4848
|
-
* listed here
|
|
4849
|
-
*
|
|
4850
|
-
* `
|
|
4851
|
-
* result; it needs its own handler in the generator and is left for a
|
|
4852
|
-
* follow-up. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4848
|
+
* listed here except `stop`: it has no `agent_before_settle`, so `stop` maps to
|
|
4849
|
+
* its own `session_stop` event instead. It has no `ui_prompt_start` either, so
|
|
4850
|
+
* `notification` is not mapped. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4853
4851
|
*
|
|
4854
|
-
* @see https://github.com/can1357/oh-my-pi/blob/
|
|
4852
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
4855
4853
|
*/
|
|
4856
4854
|
const OMP_HOOK_EVENTS = [
|
|
4857
4855
|
"sessionStart",
|
|
@@ -4862,6 +4860,7 @@ const OMP_HOOK_EVENTS = [
|
|
|
4862
4860
|
"preModelInvocation",
|
|
4863
4861
|
"postModelInvocation",
|
|
4864
4862
|
"beforeSubmitPrompt",
|
|
4863
|
+
"stop",
|
|
4865
4864
|
"preCompact",
|
|
4866
4865
|
"postCompact"
|
|
4867
4866
|
];
|
|
@@ -6283,9 +6282,10 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
|
|
|
6283
6282
|
/**
|
|
6284
6283
|
* Map canonical camelCase event names to oh-my-pi extension event names. They
|
|
6285
6284
|
* are Pi's names (see CANONICAL_TO_PI_EVENT_NAMES) for the events oh-my-pi
|
|
6286
|
-
* kept
|
|
6287
|
-
* budget, and fails a `tool_call`
|
|
6288
|
-
* `extensionHandlers.toolCallTimeoutMs`
|
|
6285
|
+
* kept, plus `session_stop` for `stop`. Note that oh-my-pi runs
|
|
6286
|
+
* `session_shutdown` handlers with a 2-second budget, and fails a `tool_call`
|
|
6287
|
+
* closed when its handler exceeds `extensionHandlers.toolCallTimeoutMs`
|
|
6288
|
+
* (30 seconds by default).
|
|
6289
6289
|
*
|
|
6290
6290
|
* @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
|
|
6291
6291
|
*/
|
|
@@ -6298,6 +6298,7 @@ const CANONICAL_TO_OMP_EVENT_NAMES = {
|
|
|
6298
6298
|
preModelInvocation: "context",
|
|
6299
6299
|
postModelInvocation: "message_end",
|
|
6300
6300
|
beforeSubmitPrompt: "input",
|
|
6301
|
+
stop: "session_stop",
|
|
6301
6302
|
preCompact: "session_before_compact",
|
|
6302
6303
|
postCompact: "session_compact"
|
|
6303
6304
|
};
|
|
@@ -12955,10 +12956,16 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12955
12956
|
[CODEWHALE_CONFIG_SHARED_FILE_KEY]: {
|
|
12956
12957
|
format: "toml",
|
|
12957
12958
|
invalidRootPolicy: "error",
|
|
12958
|
-
features: {
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12959
|
+
features: {
|
|
12960
|
+
hooks: {
|
|
12961
|
+
kind: "replace-owned-keys",
|
|
12962
|
+
ownedKeys: ["hooks"]
|
|
12963
|
+
},
|
|
12964
|
+
permissions: {
|
|
12965
|
+
kind: "replace-owned-keys",
|
|
12966
|
+
ownedKeys: ["allow_shell"]
|
|
12967
|
+
}
|
|
12968
|
+
}
|
|
12962
12969
|
},
|
|
12963
12970
|
".letta/settings.json": {
|
|
12964
12971
|
format: "json",
|
|
@@ -32857,6 +32864,31 @@ const PI_PROMPT_BLOCKING_EVENT = "input";
|
|
|
32857
32864
|
* @see https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/core/extensions/types.ts
|
|
32858
32865
|
*/
|
|
32859
32866
|
const PI_SETTLE_BLOCKING_EVENT = "agent_before_settle";
|
|
32867
|
+
/**
|
|
32868
|
+
* `session_stop` is oh-my-pi's stop surface (it has no `agent_before_settle`).
|
|
32869
|
+
* It fires when a main-session run is about to settle (never for subagents,
|
|
32870
|
+
* nor for a run the user aborted), and a handler returning
|
|
32871
|
+
* `{ continue: true, additionalContext }` queues one more turn with that text
|
|
32872
|
+
* as hidden model-visible context. As on Pi, only a stop command exiting with
|
|
32873
|
+
* code 2 asks for that, the feedback of every such command is combined, and
|
|
32874
|
+
* any other failure is rethrown for oh-my-pi to report. The generated handler
|
|
32875
|
+
* continues only when `event.stop_hook_active` is false, which oh-my-pi sets
|
|
32876
|
+
* after a continuation and clears once a stop pass lets the run settle, so the
|
|
32877
|
+
* agent is continued at most once per stop (the flag is shared by every
|
|
32878
|
+
* extension, so another extension's continuation also suppresses ours);
|
|
32879
|
+
* oh-my-pi additionally caps these
|
|
32880
|
+
* advisory continuations at 8 in a row. A run that ended on a provider error
|
|
32881
|
+
* (`stopReason: "error"`) is not continued, matching Pi's `completed`-only
|
|
32882
|
+
* gate. The advisory form is used rather than `{ decision: "block", reason }`,
|
|
32883
|
+
* which oh-my-pi keeps enforcing without that cap. oh-my-pi gives
|
|
32884
|
+
* `session_stop` handlers the 30-second extension handler budget: stop
|
|
32885
|
+
* commands running longer time the handler out, the run settles without a
|
|
32886
|
+
* continuation, and the command is not killed.
|
|
32887
|
+
*
|
|
32888
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
32889
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/packages/coding-agent/src/session/agent-session.ts
|
|
32890
|
+
*/
|
|
32891
|
+
const OMP_SESSION_STOP_BLOCKING_EVENT = "session_stop";
|
|
32860
32892
|
const PI_EXTENSION_DIALECT = {
|
|
32861
32893
|
overrideKey: "pi",
|
|
32862
32894
|
typesPackage: "@earendil-works/pi-coding-agent",
|
|
@@ -32878,7 +32910,8 @@ const OMP_EXTENSION_DIALECT = {
|
|
|
32878
32910
|
const PI_BLOCKING_MODE_BY_EVENT = {
|
|
32879
32911
|
[PI_BLOCKING_EVENT]: "tool",
|
|
32880
32912
|
[PI_PROMPT_BLOCKING_EVENT]: "prompt",
|
|
32881
|
-
[PI_SETTLE_BLOCKING_EVENT]: "settle"
|
|
32913
|
+
[PI_SETTLE_BLOCKING_EVENT]: "settle",
|
|
32914
|
+
[OMP_SESSION_STOP_BLOCKING_EVENT]: "sessionStop"
|
|
32882
32915
|
};
|
|
32883
32916
|
/**
|
|
32884
32917
|
* The body emitted inside a blocking handler's `catch`, per gate. `tool_call`
|
|
@@ -32891,7 +32924,8 @@ function failureLinesByMode(dialect) {
|
|
|
32891
32924
|
none: [],
|
|
32892
32925
|
tool: ["return { block: true, reason: toBlockReason(error) };"],
|
|
32893
32926
|
prompt: ["reportPromptGateFailure(ctx, toBlockReason(error));", dialect.promptCancelLine],
|
|
32894
|
-
settle: ["stopHookFailures.push(error);"]
|
|
32927
|
+
settle: ["stopHookFailures.push(error);"],
|
|
32928
|
+
sessionStop: ["stopHookFailures.push(error);"]
|
|
32895
32929
|
};
|
|
32896
32930
|
}
|
|
32897
32931
|
/**
|
|
@@ -32983,6 +33017,12 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32983
33017
|
" console.error(reason);",
|
|
32984
33018
|
"}"
|
|
32985
33019
|
];
|
|
33020
|
+
/** Helper emitted alongside every stop gate (Pi's settle gate and oh-my-pi's `session_stop`). */
|
|
33021
|
+
const STOP_HOOK_BLOCK_HELPER_LINES = [
|
|
33022
|
+
"function isStopHookBlock(error: unknown): boolean {",
|
|
33023
|
+
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
33024
|
+
"}"
|
|
33025
|
+
];
|
|
32986
33026
|
/**
|
|
32987
33027
|
* Helper emitted alongside the stop hook's settle gate. `event.entries` holds
|
|
32988
33028
|
* the drafts earlier extensions proposed at this boundary, so they are kept and
|
|
@@ -32993,10 +33033,6 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32993
33033
|
* appended message is committed, and Pi re-validates the final context itself.
|
|
32994
33034
|
*/
|
|
32995
33035
|
const STOP_HOOK_HELPER_LINES = [
|
|
32996
|
-
"function isStopHookBlock(error: unknown): boolean {",
|
|
32997
|
-
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
32998
|
-
"}",
|
|
32999
|
-
"",
|
|
33000
33036
|
"function continueAfterStopHook(",
|
|
33001
33037
|
" event: AgentBeforeSettleEvent,",
|
|
33002
33038
|
" reason: string,",
|
|
@@ -33079,14 +33115,16 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33079
33115
|
const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
|
|
33080
33116
|
const isPromptGate = blocking === "prompt";
|
|
33081
33117
|
const isSettleGate = blocking === "settle";
|
|
33118
|
+
const isSessionStopGate = blocking === "sessionStop";
|
|
33119
|
+
const collectsStopFailures = isSettleGate || isSessionStopGate;
|
|
33082
33120
|
const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
|
|
33083
33121
|
const usesErrorFlag = handlers.some((h) => h.onlyOnError);
|
|
33084
33122
|
const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
|
|
33085
|
-
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate ||
|
|
33123
|
+
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || collectsStopFailures ? "event" : "";
|
|
33086
33124
|
lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
|
|
33087
33125
|
if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
|
|
33088
33126
|
if (isPromptGate) lines.push(` if (event.source === "extension") ${dialect.promptPassLine}`);
|
|
33089
|
-
if (
|
|
33127
|
+
if (collectsStopFailures) lines.push(" const stopHookFailures: unknown[] = [];");
|
|
33090
33128
|
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
33091
33129
|
handler,
|
|
33092
33130
|
usesToolName,
|
|
@@ -33102,6 +33140,17 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33102
33140
|
lines.push(" }");
|
|
33103
33141
|
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33104
33142
|
}
|
|
33143
|
+
if (isSessionStopGate) {
|
|
33144
|
+
lines.push(" const blocks = stopHookFailures.filter(isStopHookBlock);");
|
|
33145
|
+
lines.push(" const endedOnError = (event.last_assistant_message as { stopReason?: unknown } | undefined)?.stopReason === \"error\";");
|
|
33146
|
+
lines.push(" if (blocks.length > 0 && !event.stop_hook_active && !endedOnError) {");
|
|
33147
|
+
lines.push(" return {");
|
|
33148
|
+
lines.push(" continue: true,");
|
|
33149
|
+
lines.push(" additionalContext: `Stop hook feedback:\\n${blocks.map(toBlockReason).join(\"\\n\\n\")}`,");
|
|
33150
|
+
lines.push(" };");
|
|
33151
|
+
lines.push(" }");
|
|
33152
|
+
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33153
|
+
}
|
|
33105
33154
|
lines.push(" });");
|
|
33106
33155
|
if (isSettleGate) {
|
|
33107
33156
|
lines.push(` pi.on("agent_settled", async () => {`);
|
|
@@ -33120,8 +33169,9 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33120
33169
|
* `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
|
|
33121
33170
|
* where a non-zero exit cancels the prompt with the dialect's cancel result
|
|
33122
33171
|
* (`{ action: "handled" }` for Pi, `{ handled: true }` for oh-my-pi), and
|
|
33123
|
-
* on `agent_before_settle
|
|
33124
|
-
* agent to continue once with the
|
|
33172
|
+
* on `agent_before_settle` (Pi) / `session_stop` (oh-my-pi), where a stop
|
|
33173
|
+
* command exiting with code 2 asks the agent to continue once with the
|
|
33174
|
+
* command's output as feedback.
|
|
33125
33175
|
* `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
|
|
33126
33176
|
* latter's commands run only when `event.isError` is set.
|
|
33127
33177
|
*
|
|
@@ -33142,7 +33192,8 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33142
33192
|
const subscriptionLines = buildSubscriptionLines(handlerGroups, dialect);
|
|
33143
33193
|
const hasPromptGate = Boolean(handlerGroups[PI_PROMPT_BLOCKING_EVENT]);
|
|
33144
33194
|
const hasSettleGate = Boolean(handlerGroups[PI_SETTLE_BLOCKING_EVENT]);
|
|
33145
|
-
const
|
|
33195
|
+
const hasSessionStopGate = Boolean(handlerGroups[OMP_SESSION_STOP_BLOCKING_EVENT]);
|
|
33196
|
+
const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate || hasSessionStopGate;
|
|
33146
33197
|
const lines = ["// Generated by rulesync. Do not edit manually."];
|
|
33147
33198
|
if (subscriptionLines.length === 0) {
|
|
33148
33199
|
lines.push("export default function () {}");
|
|
@@ -33169,6 +33220,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33169
33220
|
lines.push(...PROMPT_GATE_HELPER_LINES);
|
|
33170
33221
|
lines.push("");
|
|
33171
33222
|
}
|
|
33223
|
+
if (hasSettleGate || hasSessionStopGate) {
|
|
33224
|
+
lines.push(...STOP_HOOK_BLOCK_HELPER_LINES);
|
|
33225
|
+
lines.push("");
|
|
33226
|
+
}
|
|
33172
33227
|
if (hasSettleGate) {
|
|
33173
33228
|
lines.push(...STOP_HOOK_HELPER_LINES);
|
|
33174
33229
|
lines.push("");
|
|
@@ -55965,6 +56020,124 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
|
|
|
55965
56020
|
}
|
|
55966
56021
|
}
|
|
55967
56022
|
//#endregion
|
|
56023
|
+
//#region src/features/permissions/codewhale-project-config.ts
|
|
56024
|
+
/**
|
|
56025
|
+
* The top-level key of the per-project `.codewhale/config.toml` overlay that
|
|
56026
|
+
* hides Codewhale's shell tools. The overlay only tightens: `false` disables
|
|
56027
|
+
* shell for the session, while `true` is ignored with a warning.
|
|
56028
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/docs/CONFIGURATION.md#per-project-overlay-485
|
|
56029
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/crates/tui/src/lib.rs
|
|
56030
|
+
*/
|
|
56031
|
+
const CODEWHALE_ALLOW_SHELL_KEY = "allow_shell";
|
|
56032
|
+
const CODEWHALE_PROJECT_CONFIG_PATHS = {
|
|
56033
|
+
relativeDirPath: CODEWHALE_DIR,
|
|
56034
|
+
relativeFilePath: CODEWHALE_CONFIG_FILE_NAME
|
|
56035
|
+
};
|
|
56036
|
+
/** The project `.codewhale/config.toml` overlay, as written by the permissions feature. */
|
|
56037
|
+
var CodewhaleProjectConfigFile = class extends ToolFile {
|
|
56038
|
+
validate() {
|
|
56039
|
+
return {
|
|
56040
|
+
success: true,
|
|
56041
|
+
error: null
|
|
56042
|
+
};
|
|
56043
|
+
}
|
|
56044
|
+
};
|
|
56045
|
+
/**
|
|
56046
|
+
* Whether the canonical config denies every shell command: a `deny` on the
|
|
56047
|
+
* catch-all `*` (or `**`) pattern of `bash` or of the all-tools `*` category.
|
|
56048
|
+
* Codewhale decides by action before specificity, so such a deny already
|
|
56049
|
+
* blocks every command in global scope too, whatever `allow` rules sit beside
|
|
56050
|
+
* it.
|
|
56051
|
+
*/
|
|
56052
|
+
function deniesEveryShellCommand(permission) {
|
|
56053
|
+
return collectShellCommandRules(permission).rules.some(({ pattern, action }) => action === "deny" && isShellCatchAll(pattern));
|
|
56054
|
+
}
|
|
56055
|
+
/** Whether a canonical shell pattern names every command. */
|
|
56056
|
+
function isShellCatchAll(pattern) {
|
|
56057
|
+
const trimmed = pattern.trim();
|
|
56058
|
+
return trimmed === "*" || trimmed === "**";
|
|
56059
|
+
}
|
|
56060
|
+
function projectConfigFilePath(outputRoot) {
|
|
56061
|
+
return (0, node_path.join)(outputRoot, CODEWHALE_PROJECT_CONFIG_PATHS.relativeDirPath, CODEWHALE_PROJECT_CONFIG_PATHS.relativeFilePath);
|
|
56062
|
+
}
|
|
56063
|
+
function parseProjectConfig({ fileContent, filePath }) {
|
|
56064
|
+
return parseSharedConfig({
|
|
56065
|
+
format: "toml",
|
|
56066
|
+
fileContent,
|
|
56067
|
+
filePath,
|
|
56068
|
+
invalidRootPolicy: "error"
|
|
56069
|
+
});
|
|
56070
|
+
}
|
|
56071
|
+
/**
|
|
56072
|
+
* Whether an overlay disables shell (`allow_shell = false`). A file that
|
|
56073
|
+
* cannot be read as TOML is reported and treated as not disabling shell.
|
|
56074
|
+
*/
|
|
56075
|
+
function disablesShell({ fileContent, filePath, logger }) {
|
|
56076
|
+
try {
|
|
56077
|
+
return parseProjectConfig({
|
|
56078
|
+
fileContent,
|
|
56079
|
+
filePath
|
|
56080
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false;
|
|
56081
|
+
} catch (error) {
|
|
56082
|
+
logger?.warn(`Codewhale permissions: not reading "allow_shell" from ${filePath}, so a shell deny set there is not taken into account: ${formatError(error)}`);
|
|
56083
|
+
return false;
|
|
56084
|
+
}
|
|
56085
|
+
}
|
|
56086
|
+
/**
|
|
56087
|
+
* Whether the project overlay at `outputRoot` disables shell
|
|
56088
|
+
* (`allow_shell = false`), for import.
|
|
56089
|
+
*/
|
|
56090
|
+
async function readCodewhaleProjectShellDisabled({ outputRoot, logger }) {
|
|
56091
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56092
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
56093
|
+
if (fileContent === null) return false;
|
|
56094
|
+
return disablesShell({
|
|
56095
|
+
fileContent,
|
|
56096
|
+
filePath,
|
|
56097
|
+
logger
|
|
56098
|
+
});
|
|
56099
|
+
}
|
|
56100
|
+
/**
|
|
56101
|
+
* The project `.codewhale/config.toml` overlay carrying `allow_shell = false`
|
|
56102
|
+
* when the canonical config denies every shell command, or `null` when the
|
|
56103
|
+
* file needs no change.
|
|
56104
|
+
*
|
|
56105
|
+
* Only `allow_shell` is written; every other overlay key (`model`,
|
|
56106
|
+
* `approval_policy`, ...) is kept, and an existing file that is not valid TOML
|
|
56107
|
+
* is an error only when it has to be written. An existing
|
|
56108
|
+
* `allow_shell = false` is never removed: the overlay cannot widen access, and
|
|
56109
|
+
* a hand-written value cannot be told apart from one rulesync wrote, so it is
|
|
56110
|
+
* reported instead.
|
|
56111
|
+
*/
|
|
56112
|
+
async function createCodewhaleProjectConfigFile({ outputRoot = process.cwd(), config, logger }) {
|
|
56113
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56114
|
+
const existingContent = await readFileContentOrNull(filePath);
|
|
56115
|
+
if (!deniesEveryShellCommand(config.permission)) {
|
|
56116
|
+
if (existingContent !== null && disablesShell({
|
|
56117
|
+
fileContent: existingContent,
|
|
56118
|
+
filePath,
|
|
56119
|
+
logger
|
|
56120
|
+
})) logger?.warn(`Codewhale permissions: keeping "allow_shell = false" in ${filePath}; rulesync does not remove it. Delete it by hand to re-enable shell tools in this project, or add bash: { "*": "deny" } to the rulesync permissions to match it.`);
|
|
56121
|
+
return null;
|
|
56122
|
+
}
|
|
56123
|
+
if (parseProjectConfig({
|
|
56124
|
+
fileContent: existingContent ?? "",
|
|
56125
|
+
filePath
|
|
56126
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false) return null;
|
|
56127
|
+
return new CodewhaleProjectConfigFile({
|
|
56128
|
+
outputRoot,
|
|
56129
|
+
...CODEWHALE_PROJECT_CONFIG_PATHS,
|
|
56130
|
+
fileContent: applySharedConfigPatch({
|
|
56131
|
+
fileKey: sharedConfigFileKey(CODEWHALE_PROJECT_CONFIG_PATHS),
|
|
56132
|
+
feature: "permissions",
|
|
56133
|
+
existingContent: existingContent ?? "",
|
|
56134
|
+
patch: { [CODEWHALE_ALLOW_SHELL_KEY]: false },
|
|
56135
|
+
filePath,
|
|
56136
|
+
logger
|
|
56137
|
+
})
|
|
56138
|
+
});
|
|
56139
|
+
}
|
|
56140
|
+
//#endregion
|
|
55968
56141
|
//#region src/features/permissions/codewhale-repo-law.ts
|
|
55969
56142
|
/**
|
|
55970
56143
|
* Project-scope Codewhale permissions: the enforced `protected_invariants` of
|
|
@@ -56115,7 +56288,8 @@ function collectRestrictions({ config, logger }) {
|
|
|
56115
56288
|
let skippedAllowCount = 0;
|
|
56116
56289
|
for (const [category, rules] of Object.entries(config.permission)) {
|
|
56117
56290
|
if (!REPO_LAW_CATEGORIES.includes(category)) {
|
|
56118
|
-
|
|
56291
|
+
const entries = Object.entries(rules);
|
|
56292
|
+
if ((category === "bash" && entries.some(([pattern, action]) => action === "deny" && isShellCatchAll(pattern)) ? entries.filter(([, action]) => action === "allow") : entries).length > 0) skippedCategories.push(category);
|
|
56119
56293
|
continue;
|
|
56120
56294
|
}
|
|
56121
56295
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -56841,17 +57015,32 @@ async function warnOnShadowedConstitution({ outputRoot, logger }) {
|
|
|
56841
57015
|
* Project scope manages the enforced `protected_invariants` of the repo
|
|
56842
57016
|
* constitution `.codewhale/constitution.json` (see `codewhale-repo-law.ts`):
|
|
56843
57017
|
* the project `.codewhale/config.toml` overlay cannot carry rules, but repo
|
|
56844
|
-
* law can hold writes to protected paths.
|
|
57018
|
+
* law can hold writes to protected paths. A deny on every shell command also
|
|
57019
|
+
* sets `allow_shell = false` in that overlay (see
|
|
57020
|
+
* `codewhale-project-config.ts`), which import reads back here.
|
|
56845
57021
|
*
|
|
56846
57022
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
|
|
56847
57023
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
|
|
56848
57024
|
*/
|
|
56849
57025
|
var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
56850
|
-
|
|
57026
|
+
/**
|
|
57027
|
+
* Whether the project `.codewhale/config.toml` overlay disables shell
|
|
57028
|
+
* (`allow_shell = false`); read on import in project scope only.
|
|
57029
|
+
*/
|
|
57030
|
+
projectShellDisabled;
|
|
57031
|
+
constructor({ projectShellDisabled = false, ...params }) {
|
|
56851
57032
|
super({
|
|
56852
57033
|
...params,
|
|
56853
57034
|
fileContent: params.fileContent ?? ""
|
|
56854
57035
|
});
|
|
57036
|
+
this.projectShellDisabled = projectShellDisabled;
|
|
57037
|
+
}
|
|
57038
|
+
/**
|
|
57039
|
+
* The project `.codewhale/config.toml` overlay, where a deny on every shell
|
|
57040
|
+
* command becomes `allow_shell = false` beside the user's other overlay keys.
|
|
57041
|
+
*/
|
|
57042
|
+
static getExtraSharedWritePaths(options) {
|
|
57043
|
+
return options?.global ? [] : [{ ...CODEWHALE_PROJECT_CONFIG_PATHS }];
|
|
56855
57044
|
}
|
|
56856
57045
|
/**
|
|
56857
57046
|
* Codewhale's approval card writes to the global file, and the constitution
|
|
@@ -56888,7 +57077,11 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56888
57077
|
relativeFilePath: paths.relativeFilePath,
|
|
56889
57078
|
fileContent,
|
|
56890
57079
|
validate,
|
|
56891
|
-
global
|
|
57080
|
+
global,
|
|
57081
|
+
projectShellDisabled: global ? false : await readCodewhaleProjectShellDisabled({
|
|
57082
|
+
outputRoot,
|
|
57083
|
+
logger
|
|
57084
|
+
})
|
|
56892
57085
|
});
|
|
56893
57086
|
}
|
|
56894
57087
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
@@ -56950,11 +57143,15 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56950
57143
|
toRulesyncPermissions() {
|
|
56951
57144
|
const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
56952
57145
|
if (!this.global) {
|
|
56953
|
-
const
|
|
57146
|
+
const permission = codewhaleConstitutionToCanonical(parseCodewhaleConstitution({
|
|
56954
57147
|
fileContent: this.getFileContent(),
|
|
56955
57148
|
filePath
|
|
56956
|
-
});
|
|
56957
|
-
|
|
57149
|
+
}));
|
|
57150
|
+
if (this.projectShellDisabled) permission.bash = {
|
|
57151
|
+
...permission.bash,
|
|
57152
|
+
"*": "deny"
|
|
57153
|
+
};
|
|
57154
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
56958
57155
|
}
|
|
56959
57156
|
const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
|
|
56960
57157
|
fileContent: this.getFileContent(),
|
|
@@ -71883,6 +72080,14 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
71883
72080
|
logger: this.logger,
|
|
71884
72081
|
global: this.global
|
|
71885
72082
|
});
|
|
72083
|
+
if (this.toolTarget === "codewhale" && !this.global) {
|
|
72084
|
+
const projectConfigFile = await createCodewhaleProjectConfigFile({
|
|
72085
|
+
outputRoot: this.outputRoot,
|
|
72086
|
+
config: effectivePermissions.getJson(),
|
|
72087
|
+
logger: this.logger
|
|
72088
|
+
});
|
|
72089
|
+
return projectConfigFile ? [toolPermissions, projectConfigFile] : [toolPermissions];
|
|
72090
|
+
}
|
|
71886
72091
|
if (this.toolTarget !== "codexcli") return [toolPermissions];
|
|
71887
72092
|
return [toolPermissions, createCodexcliBashRulesFile({
|
|
71888
72093
|
outputRoot: this.outputRoot,
|