rulesync 31.0.0 → 33.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 +23 -5
- package/dist/cli/index.js +23 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-ORYbMDJK.js → import-5-Uennl1.js} +451 -50
- package/dist/{import-ORYbMDJK.js.map → import-5-Uennl1.js.map} +1 -1
- package/dist/{import-CBvXTNp8.cjs → import-D_SV_C0k.cjs} +467 -66
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -4823,13 +4823,11 @@ const PI_HOOK_EVENTS = [
|
|
|
4823
4823
|
/**
|
|
4824
4824
|
* Hook events supported by oh-my-pi (`omp`), Pi's fork, through the same
|
|
4825
4825
|
* generated TypeScript extension. oh-my-pi keeps Pi's names for every event
|
|
4826
|
-
* listed here
|
|
4827
|
-
*
|
|
4828
|
-
* `
|
|
4829
|
-
* result; it needs its own handler in the generator and is left for a
|
|
4830
|
-
* follow-up. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4826
|
+
* listed here except `stop`: it has no `agent_before_settle`, so `stop` maps to
|
|
4827
|
+
* its own `session_stop` event instead. It has no `ui_prompt_start` either, so
|
|
4828
|
+
* `notification` is not mapped. See CANONICAL_TO_OMP_EVENT_NAMES.
|
|
4831
4829
|
*
|
|
4832
|
-
* @see https://github.com/can1357/oh-my-pi/blob/
|
|
4830
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
4833
4831
|
*/
|
|
4834
4832
|
const OMP_HOOK_EVENTS = [
|
|
4835
4833
|
"sessionStart",
|
|
@@ -4840,6 +4838,7 @@ const OMP_HOOK_EVENTS = [
|
|
|
4840
4838
|
"preModelInvocation",
|
|
4841
4839
|
"postModelInvocation",
|
|
4842
4840
|
"beforeSubmitPrompt",
|
|
4841
|
+
"stop",
|
|
4843
4842
|
"preCompact",
|
|
4844
4843
|
"postCompact"
|
|
4845
4844
|
];
|
|
@@ -6261,9 +6260,10 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
|
|
|
6261
6260
|
/**
|
|
6262
6261
|
* Map canonical camelCase event names to oh-my-pi extension event names. They
|
|
6263
6262
|
* are Pi's names (see CANONICAL_TO_PI_EVENT_NAMES) for the events oh-my-pi
|
|
6264
|
-
* kept
|
|
6265
|
-
* budget, and fails a `tool_call`
|
|
6266
|
-
* `extensionHandlers.toolCallTimeoutMs`
|
|
6263
|
+
* kept, plus `session_stop` for `stop`. Note that oh-my-pi runs
|
|
6264
|
+
* `session_shutdown` handlers with a 2-second budget, and fails a `tool_call`
|
|
6265
|
+
* closed when its handler exceeds `extensionHandlers.toolCallTimeoutMs`
|
|
6266
|
+
* (30 seconds by default).
|
|
6267
6267
|
*
|
|
6268
6268
|
* @see https://github.com/can1357/oh-my-pi/blob/40e9368ef0458fd9073329cdff4174895f91bc6b/docs/extensions.md
|
|
6269
6269
|
*/
|
|
@@ -6276,6 +6276,7 @@ const CANONICAL_TO_OMP_EVENT_NAMES = {
|
|
|
6276
6276
|
preModelInvocation: "context",
|
|
6277
6277
|
postModelInvocation: "message_end",
|
|
6278
6278
|
beforeSubmitPrompt: "input",
|
|
6279
|
+
stop: "session_stop",
|
|
6279
6280
|
preCompact: "session_before_compact",
|
|
6280
6281
|
postCompact: "session_compact"
|
|
6281
6282
|
};
|
|
@@ -12933,10 +12934,16 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12933
12934
|
[CODEWHALE_CONFIG_SHARED_FILE_KEY]: {
|
|
12934
12935
|
format: "toml",
|
|
12935
12936
|
invalidRootPolicy: "error",
|
|
12936
|
-
features: {
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12937
|
+
features: {
|
|
12938
|
+
hooks: {
|
|
12939
|
+
kind: "replace-owned-keys",
|
|
12940
|
+
ownedKeys: ["hooks"]
|
|
12941
|
+
},
|
|
12942
|
+
permissions: {
|
|
12943
|
+
kind: "replace-owned-keys",
|
|
12944
|
+
ownedKeys: ["allow_shell"]
|
|
12945
|
+
}
|
|
12946
|
+
}
|
|
12940
12947
|
},
|
|
12941
12948
|
".letta/settings.json": {
|
|
12942
12949
|
format: "json",
|
|
@@ -21737,22 +21744,29 @@ function mergeOpencodeV2Entries({ config, legacyKey, pluralKey, lower }) {
|
|
|
21737
21744
|
const entry = asOpencodeEntries(value);
|
|
21738
21745
|
const lowered = entry === null ? null : lower(entry);
|
|
21739
21746
|
if (Object.hasOwn(merged, name) || lowered === null) continue;
|
|
21740
|
-
|
|
21741
|
-
value: lowered,
|
|
21742
|
-
enumerable: true,
|
|
21743
|
-
configurable: true,
|
|
21744
|
-
writable: true
|
|
21745
|
-
});
|
|
21747
|
+
setOwnEntry(merged, name, lowered);
|
|
21746
21748
|
}
|
|
21747
21749
|
return Object.keys(merged).length > 0 ? merged : legacy;
|
|
21748
21750
|
}
|
|
21749
21751
|
/**
|
|
21752
|
+
* Sets `target[key]` as an own, enumerable property. A plain assignment to a
|
|
21753
|
+
* key named `__proto__` would set the prototype instead.
|
|
21754
|
+
*/
|
|
21755
|
+
function setOwnEntry(target, key, value) {
|
|
21756
|
+
Object.defineProperty(target, key, {
|
|
21757
|
+
value,
|
|
21758
|
+
enumerable: true,
|
|
21759
|
+
configurable: true,
|
|
21760
|
+
writable: true
|
|
21761
|
+
});
|
|
21762
|
+
}
|
|
21763
|
+
/**
|
|
21750
21764
|
* Returns the inline agent entries of an OpenCode config in the V1 `agent`
|
|
21751
21765
|
* shape. Since v1.18.24 OpenCode V1 also reads the V2 `agents` spelling and
|
|
21752
21766
|
* lowers it, so a layout that does so (`readsV2ConfigSpellings`) gets those
|
|
21753
21767
|
* entries too.
|
|
21754
21768
|
*
|
|
21755
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21769
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21756
21770
|
*/
|
|
21757
21771
|
function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21758
21772
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.agent);
|
|
@@ -21771,7 +21785,7 @@ function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21771
21785
|
* Other fields are kept: V1 drops a V2 `subagent`, but rulesync reads it as
|
|
21772
21786
|
* `subtask` everywhere (as V2 does), so it is left for the command importer.
|
|
21773
21787
|
*
|
|
21774
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21788
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21775
21789
|
*/
|
|
21776
21790
|
function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
21777
21791
|
if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.command);
|
|
@@ -21794,7 +21808,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21794
21808
|
* reads V2 spellings, a flat V2 `skills` array is accepted too: its entries
|
|
21795
21809
|
* that are not `http(s)://` URLs are the paths, as OpenCode V1 lowers them.
|
|
21796
21810
|
*
|
|
21797
|
-
* @see https://github.com/anomalyco/opencode/blob/v1.18.
|
|
21811
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21798
21812
|
*/
|
|
21799
21813
|
function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
21800
21814
|
if (Array.isArray(config.skills)) {
|
|
@@ -21804,6 +21818,160 @@ function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
|
|
|
21804
21818
|
const skills = asOpencodeEntries(config.skills);
|
|
21805
21819
|
return skills !== null && Array.isArray(skills.paths) ? skills.paths : [];
|
|
21806
21820
|
}
|
|
21821
|
+
/**
|
|
21822
|
+
* Whether a record is itself an MCP server entry rather than a V2 container
|
|
21823
|
+
* (`mcp.servers`, `mcp.timeout`): a server — or a server literally named
|
|
21824
|
+
* `type` / `enabled` — carries a scalar `type` or `enabled`. Mirrors OpenCode
|
|
21825
|
+
* V1's `isDirectServer`.
|
|
21826
|
+
*/
|
|
21827
|
+
function isDirectOpencodeMcpServer(value) {
|
|
21828
|
+
return ["type", "enabled"].some((key) => Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])));
|
|
21829
|
+
}
|
|
21830
|
+
const OPENCODE_V2_TIMEOUT_KEYS = [
|
|
21831
|
+
"startup",
|
|
21832
|
+
"catalog",
|
|
21833
|
+
"execution"
|
|
21834
|
+
];
|
|
21835
|
+
/**
|
|
21836
|
+
* Whether an MCP server entry uses a V2-only per-server spelling, so that
|
|
21837
|
+
* OpenCode V1 lowers it: `disabled`, `codemode`, an object `timeout`, or a
|
|
21838
|
+
* snake_case `oauth` key.
|
|
21839
|
+
*/
|
|
21840
|
+
function isOpencodeV2NativeServer(value) {
|
|
21841
|
+
const oauth = value.oauth;
|
|
21842
|
+
return Object.hasOwn(value, "disabled") || Object.hasOwn(value, "codemode") || isRecord$1(value.timeout) || isRecord$1(oauth) && [
|
|
21843
|
+
"client_id",
|
|
21844
|
+
"client_secret",
|
|
21845
|
+
"callback_port",
|
|
21846
|
+
"redirect_uri"
|
|
21847
|
+
].some((key) => Object.hasOwn(oauth, key));
|
|
21848
|
+
}
|
|
21849
|
+
/**
|
|
21850
|
+
* Lowers a V2 timeout object (`{ startup, catalog, execution }`) to V1's single
|
|
21851
|
+
* number, the way OpenCode V1 does: only a `catalog` equal to `execution`, with
|
|
21852
|
+
* no `startup`, has a V1 equivalent. Anything else — including a value that is
|
|
21853
|
+
* not a positive integer — is `undefined` (dropped).
|
|
21854
|
+
*/
|
|
21855
|
+
function lowerOpencodeV2Timeout(value) {
|
|
21856
|
+
if (!hasPositiveIntegerTimeouts(value) || value.startup !== void 0 || typeof value.catalog !== "number") return;
|
|
21857
|
+
return value.catalog === value.execution ? value.catalog : void 0;
|
|
21858
|
+
}
|
|
21859
|
+
const OPENCODE_V2_OAUTH_KEYS = /* @__PURE__ */ new Map([
|
|
21860
|
+
["client_id", "clientId"],
|
|
21861
|
+
["client_secret", "clientSecret"],
|
|
21862
|
+
["callback_port", "callbackPort"],
|
|
21863
|
+
["redirect_uri", "redirectUri"]
|
|
21864
|
+
]);
|
|
21865
|
+
/**
|
|
21866
|
+
* V2 `oauth` keys that have no V1 spelling. OpenCode V1 drops them when it
|
|
21867
|
+
* lowers a server, so they are dropped (and reported) here too rather than
|
|
21868
|
+
* being written back as a snake_case key among camelCase ones.
|
|
21869
|
+
*/
|
|
21870
|
+
const OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS = /* @__PURE__ */ new Set(["auth_server_metadata_url"]);
|
|
21871
|
+
function lowerOpencodeV2OAuth(oauth, path, onDropped) {
|
|
21872
|
+
const lowered = {};
|
|
21873
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
21874
|
+
if (OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS.has(key)) {
|
|
21875
|
+
onDropped?.(`${path}.${key}`, "unsupported");
|
|
21876
|
+
continue;
|
|
21877
|
+
}
|
|
21878
|
+
setOwnEntry(lowered, OPENCODE_V2_OAUTH_KEYS.get(key) ?? key, value);
|
|
21879
|
+
}
|
|
21880
|
+
return lowered;
|
|
21881
|
+
}
|
|
21882
|
+
/**
|
|
21883
|
+
* Lowers one V2-spelled MCP server entry to V1's shape, as OpenCode V1 does:
|
|
21884
|
+
* `disabled` becomes `enabled` (an explicit `enabled` wins), an object
|
|
21885
|
+
* `timeout` becomes a number or is dropped, and snake_case `oauth` keys become
|
|
21886
|
+
* camelCase. `codemode` is kept: Rulesync passes it through where V1 drops it.
|
|
21887
|
+
*
|
|
21888
|
+
* Like V1, only an entry that is a local or remote server is lowered, and one
|
|
21889
|
+
* whose `enabled` or `disabled` is not a boolean is left as it is. Unlike V1,
|
|
21890
|
+
* such an entry is not dropped but passed on, so the schema rejects it loudly.
|
|
21891
|
+
*/
|
|
21892
|
+
function lowerOpencodeV2Server(value, path, onDropped) {
|
|
21893
|
+
if (!isRecord$1(value) || value.type !== "local" && value.type !== "remote" || Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean" || Object.hasOwn(value, "disabled") && typeof value.disabled !== "boolean") return value;
|
|
21894
|
+
const { disabled, timeout, ...rest } = value;
|
|
21895
|
+
const lowered = { ...rest };
|
|
21896
|
+
if (!Object.hasOwn(value, "enabled")) lowered.enabled = disabled !== true;
|
|
21897
|
+
else if (value.enabled === disabled) onDropped?.(`${path}.disabled`, "conflict");
|
|
21898
|
+
const lowTimeout = isRecord$1(timeout) ? lowerOpencodeV2Timeout(timeout) : timeout;
|
|
21899
|
+
if (lowTimeout !== void 0) lowered.timeout = lowTimeout;
|
|
21900
|
+
else if (isRecord$1(timeout) && Object.keys(timeout).length > 0) onDropped?.(`${path}.timeout`, "unsupported");
|
|
21901
|
+
if (isRecord$1(value.oauth)) lowered.oauth = lowerOpencodeV2OAuth(value.oauth, `${path}.oauth`, onDropped);
|
|
21902
|
+
return lowered;
|
|
21903
|
+
}
|
|
21904
|
+
function isPositiveInteger(value) {
|
|
21905
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
21906
|
+
}
|
|
21907
|
+
/** Whether each V2 timeout value a record carries is a positive integer. */
|
|
21908
|
+
function hasPositiveIntegerTimeouts(value) {
|
|
21909
|
+
return OPENCODE_V2_TIMEOUT_KEYS.every((key) => value[key] === void 0 || isPositiveInteger(value[key]));
|
|
21910
|
+
}
|
|
21911
|
+
/**
|
|
21912
|
+
* Whether `mcp.timeout` is V2's global timeout object rather than a server
|
|
21913
|
+
* named `timeout`. As in OpenCode V1, each of its values must be a positive
|
|
21914
|
+
* integer; anything else is read as a server, which the schema then rejects.
|
|
21915
|
+
*/
|
|
21916
|
+
function isOpencodeV2GlobalTimeout(value) {
|
|
21917
|
+
return isRecord$1(value) && !isDirectOpencodeMcpServer(value) && (Object.keys(value).length === 0 || OPENCODE_V2_TIMEOUT_KEYS.some((key) => Object.hasOwn(value, key))) && hasPositiveIntegerTimeouts(value);
|
|
21918
|
+
}
|
|
21919
|
+
function getOpencodeV2McpEnvelope(mcp) {
|
|
21920
|
+
const nested = mcp.servers;
|
|
21921
|
+
return isRecord$1(nested) && !isDirectOpencodeMcpServer(nested) ? nested : null;
|
|
21922
|
+
}
|
|
21923
|
+
/**
|
|
21924
|
+
* Returns the paths of the V2-only containers in an OpenCode `mcp` value that
|
|
21925
|
+
* a write in V1's flat shape does not keep: a non-empty `mcp.servers`
|
|
21926
|
+
* envelope, and a non-empty global `mcp.timeout`.
|
|
21927
|
+
*/
|
|
21928
|
+
function findOpencodeV2McpContainers(mcp) {
|
|
21929
|
+
if (!isRecord$1(mcp)) return [];
|
|
21930
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
21931
|
+
return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
|
|
21932
|
+
}
|
|
21933
|
+
/**
|
|
21934
|
+
* Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
|
|
21935
|
+
* the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
|
|
21936
|
+
*
|
|
21937
|
+
* - entries of the `mcp.servers` envelope are merged into the map, a flat entry
|
|
21938
|
+
* of the same name winning;
|
|
21939
|
+
* - V2-spelled server entries (`disabled`, an object `timeout`, snake_case
|
|
21940
|
+
* `oauth` keys) are lowered by {@link lowerOpencodeV2Server};
|
|
21941
|
+
* - the global `mcp.timeout` object is skipped. V1 moves it to
|
|
21942
|
+
* `experimental.mcp_timeout`, a default request timeout that has no
|
|
21943
|
+
* canonical counterpart, so it is not read as a server either.
|
|
21944
|
+
*
|
|
21945
|
+
* Unlike V1, an envelope entry that is not a valid server is not dropped: it
|
|
21946
|
+
* is passed on, so the schema rejects it loudly rather than it vanishing
|
|
21947
|
+
* without a word. Each value that is dropped — a non-empty global timeout, a
|
|
21948
|
+
* per-server timeout object or `oauth` key with no V1 equivalent, or an
|
|
21949
|
+
* envelope entry shadowed by a different flat entry — is reported through
|
|
21950
|
+
* `onDropped`.
|
|
21951
|
+
*
|
|
21952
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
|
|
21953
|
+
* @see https://opencode.ai/v2/docs/mcp-servers/
|
|
21954
|
+
*/
|
|
21955
|
+
function lowerOpencodeV2Mcp(mcp, onDropped) {
|
|
21956
|
+
if (!isRecord$1(mcp)) return mcp;
|
|
21957
|
+
const envelope = getOpencodeV2McpEnvelope(mcp);
|
|
21958
|
+
const globalTimeout = isOpencodeV2GlobalTimeout(mcp.timeout);
|
|
21959
|
+
if (findOpencodeV2McpContainers(mcp).includes("mcp.timeout")) onDropped?.("mcp.timeout", "unsupported");
|
|
21960
|
+
const servers = {};
|
|
21961
|
+
for (const [name, value] of Object.entries(mcp)) {
|
|
21962
|
+
if (name === "servers" && envelope !== null || name === "timeout" && globalTimeout) continue;
|
|
21963
|
+
setOwnEntry(servers, name, isRecord$1(value) && isOpencodeV2NativeServer(value) ? lowerOpencodeV2Server(value, `mcp.${name}`, onDropped) : value);
|
|
21964
|
+
}
|
|
21965
|
+
for (const [name, value] of Object.entries(envelope ?? {})) {
|
|
21966
|
+
const path = `mcp.servers.${name}`;
|
|
21967
|
+
if (Object.hasOwn(servers, name)) {
|
|
21968
|
+
if (!isDeepStrictEqual(servers[name], value)) onDropped?.(path, "conflict");
|
|
21969
|
+
continue;
|
|
21970
|
+
}
|
|
21971
|
+
setOwnEntry(servers, name, lowerOpencodeV2Server(value, path, onDropped));
|
|
21972
|
+
}
|
|
21973
|
+
return servers;
|
|
21974
|
+
}
|
|
21807
21975
|
/** Matches a string whose entire value is an OpenCode `{file:...}` reference. */
|
|
21808
21976
|
const OPENCODE_FILE_TEMPLATE_PATTERN = /^\s*\{file:(.+?)\}\s*$/;
|
|
21809
21977
|
/**
|
|
@@ -32835,6 +33003,31 @@ const PI_PROMPT_BLOCKING_EVENT = "input";
|
|
|
32835
33003
|
* @see https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/src/core/extensions/types.ts
|
|
32836
33004
|
*/
|
|
32837
33005
|
const PI_SETTLE_BLOCKING_EVENT = "agent_before_settle";
|
|
33006
|
+
/**
|
|
33007
|
+
* `session_stop` is oh-my-pi's stop surface (it has no `agent_before_settle`).
|
|
33008
|
+
* It fires when a main-session run is about to settle (never for subagents,
|
|
33009
|
+
* nor for a run the user aborted), and a handler returning
|
|
33010
|
+
* `{ continue: true, additionalContext }` queues one more turn with that text
|
|
33011
|
+
* as hidden model-visible context. As on Pi, only a stop command exiting with
|
|
33012
|
+
* code 2 asks for that, the feedback of every such command is combined, and
|
|
33013
|
+
* any other failure is rethrown for oh-my-pi to report. The generated handler
|
|
33014
|
+
* continues only when `event.stop_hook_active` is false, which oh-my-pi sets
|
|
33015
|
+
* after a continuation and clears once a stop pass lets the run settle, so the
|
|
33016
|
+
* agent is continued at most once per stop (the flag is shared by every
|
|
33017
|
+
* extension, so another extension's continuation also suppresses ours);
|
|
33018
|
+
* oh-my-pi additionally caps these
|
|
33019
|
+
* advisory continuations at 8 in a row. A run that ended on a provider error
|
|
33020
|
+
* (`stopReason: "error"`) is not continued, matching Pi's `completed`-only
|
|
33021
|
+
* gate. The advisory form is used rather than `{ decision: "block", reason }`,
|
|
33022
|
+
* which oh-my-pi keeps enforcing without that cap. oh-my-pi gives
|
|
33023
|
+
* `session_stop` handlers the 30-second extension handler budget: stop
|
|
33024
|
+
* commands running longer time the handler out, the run settles without a
|
|
33025
|
+
* continuation, and the command is not killed.
|
|
33026
|
+
*
|
|
33027
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/docs/extensions.md
|
|
33028
|
+
* @see https://github.com/can1357/oh-my-pi/blob/fa5ff4a3a977a28b93d025f4cf50f046e019f18c/packages/coding-agent/src/session/agent-session.ts
|
|
33029
|
+
*/
|
|
33030
|
+
const OMP_SESSION_STOP_BLOCKING_EVENT = "session_stop";
|
|
32838
33031
|
const PI_EXTENSION_DIALECT = {
|
|
32839
33032
|
overrideKey: "pi",
|
|
32840
33033
|
typesPackage: "@earendil-works/pi-coding-agent",
|
|
@@ -32856,7 +33049,8 @@ const OMP_EXTENSION_DIALECT = {
|
|
|
32856
33049
|
const PI_BLOCKING_MODE_BY_EVENT = {
|
|
32857
33050
|
[PI_BLOCKING_EVENT]: "tool",
|
|
32858
33051
|
[PI_PROMPT_BLOCKING_EVENT]: "prompt",
|
|
32859
|
-
[PI_SETTLE_BLOCKING_EVENT]: "settle"
|
|
33052
|
+
[PI_SETTLE_BLOCKING_EVENT]: "settle",
|
|
33053
|
+
[OMP_SESSION_STOP_BLOCKING_EVENT]: "sessionStop"
|
|
32860
33054
|
};
|
|
32861
33055
|
/**
|
|
32862
33056
|
* The body emitted inside a blocking handler's `catch`, per gate. `tool_call`
|
|
@@ -32869,7 +33063,8 @@ function failureLinesByMode(dialect) {
|
|
|
32869
33063
|
none: [],
|
|
32870
33064
|
tool: ["return { block: true, reason: toBlockReason(error) };"],
|
|
32871
33065
|
prompt: ["reportPromptGateFailure(ctx, toBlockReason(error));", dialect.promptCancelLine],
|
|
32872
|
-
settle: ["stopHookFailures.push(error);"]
|
|
33066
|
+
settle: ["stopHookFailures.push(error);"],
|
|
33067
|
+
sessionStop: ["stopHookFailures.push(error);"]
|
|
32873
33068
|
};
|
|
32874
33069
|
}
|
|
32875
33070
|
/**
|
|
@@ -32961,6 +33156,12 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32961
33156
|
" console.error(reason);",
|
|
32962
33157
|
"}"
|
|
32963
33158
|
];
|
|
33159
|
+
/** Helper emitted alongside every stop gate (Pi's settle gate and oh-my-pi's `session_stop`). */
|
|
33160
|
+
const STOP_HOOK_BLOCK_HELPER_LINES = [
|
|
33161
|
+
"function isStopHookBlock(error: unknown): boolean {",
|
|
33162
|
+
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
33163
|
+
"}"
|
|
33164
|
+
];
|
|
32964
33165
|
/**
|
|
32965
33166
|
* Helper emitted alongside the stop hook's settle gate. `event.entries` holds
|
|
32966
33167
|
* the drafts earlier extensions proposed at this boundary, so they are kept and
|
|
@@ -32971,10 +33172,6 @@ const PROMPT_GATE_HELPER_LINES = [
|
|
|
32971
33172
|
* appended message is committed, and Pi re-validates the final context itself.
|
|
32972
33173
|
*/
|
|
32973
33174
|
const STOP_HOOK_HELPER_LINES = [
|
|
32974
|
-
"function isStopHookBlock(error: unknown): boolean {",
|
|
32975
|
-
" return (error as { code?: unknown } | null)?.code === 2;",
|
|
32976
|
-
"}",
|
|
32977
|
-
"",
|
|
32978
33175
|
"function continueAfterStopHook(",
|
|
32979
33176
|
" event: AgentBeforeSettleEvent,",
|
|
32980
33177
|
" reason: string,",
|
|
@@ -33057,14 +33254,16 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33057
33254
|
const blocking = PI_BLOCKING_MODE_BY_EVENT[piEvent] ?? "none";
|
|
33058
33255
|
const isPromptGate = blocking === "prompt";
|
|
33059
33256
|
const isSettleGate = blocking === "settle";
|
|
33257
|
+
const isSessionStopGate = blocking === "sessionStop";
|
|
33258
|
+
const collectsStopFailures = isSettleGate || isSessionStopGate;
|
|
33060
33259
|
const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
|
|
33061
33260
|
const usesErrorFlag = handlers.some((h) => h.onlyOnError);
|
|
33062
33261
|
const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
|
|
33063
|
-
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate ||
|
|
33262
|
+
const params = isPromptGate ? "event, ctx" : usesToolName || usesErrorFlag || gatesOnAssistant || isPromptGate || collectsStopFailures ? "event" : "";
|
|
33064
33263
|
lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${params}) => {`);
|
|
33065
33264
|
if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
|
|
33066
33265
|
if (isPromptGate) lines.push(` if (event.source === "extension") ${dialect.promptPassLine}`);
|
|
33067
|
-
if (
|
|
33266
|
+
if (collectsStopFailures) lines.push(" const stopHookFailures: unknown[] = [];");
|
|
33068
33267
|
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
33069
33268
|
handler,
|
|
33070
33269
|
usesToolName,
|
|
@@ -33080,6 +33279,17 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33080
33279
|
lines.push(" }");
|
|
33081
33280
|
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33082
33281
|
}
|
|
33282
|
+
if (isSessionStopGate) {
|
|
33283
|
+
lines.push(" const blocks = stopHookFailures.filter(isStopHookBlock);");
|
|
33284
|
+
lines.push(" const endedOnError = (event.last_assistant_message as { stopReason?: unknown } | undefined)?.stopReason === \"error\";");
|
|
33285
|
+
lines.push(" if (blocks.length > 0 && !event.stop_hook_active && !endedOnError) {");
|
|
33286
|
+
lines.push(" return {");
|
|
33287
|
+
lines.push(" continue: true,");
|
|
33288
|
+
lines.push(" additionalContext: `Stop hook feedback:\\n${blocks.map(toBlockReason).join(\"\\n\\n\")}`,");
|
|
33289
|
+
lines.push(" };");
|
|
33290
|
+
lines.push(" }");
|
|
33291
|
+
lines.push(" if (stopHookFailures.length > 0) throw stopHookFailures[0];");
|
|
33292
|
+
}
|
|
33083
33293
|
lines.push(" });");
|
|
33084
33294
|
if (isSettleGate) {
|
|
33085
33295
|
lines.push(` pi.on("agent_settled", async () => {`);
|
|
@@ -33098,8 +33308,9 @@ function buildSubscriptionLines(handlerGroups, dialect) {
|
|
|
33098
33308
|
* `{ block: true, reason }`, and on `input` — Pi's prompt-submission gate —
|
|
33099
33309
|
* where a non-zero exit cancels the prompt with the dialect's cancel result
|
|
33100
33310
|
* (`{ action: "handled" }` for Pi, `{ handled: true }` for oh-my-pi), and
|
|
33101
|
-
* on `agent_before_settle
|
|
33102
|
-
* agent to continue once with the
|
|
33311
|
+
* on `agent_before_settle` (Pi) / `session_stop` (oh-my-pi), where a stop
|
|
33312
|
+
* command exiting with code 2 asks the agent to continue once with the
|
|
33313
|
+
* command's output as feedback.
|
|
33103
33314
|
* `postToolUse` and `postToolUseFailure` share Pi's `tool_result` event; the
|
|
33104
33315
|
* latter's commands run only when `event.isError` is set.
|
|
33105
33316
|
*
|
|
@@ -33120,7 +33331,8 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33120
33331
|
const subscriptionLines = buildSubscriptionLines(handlerGroups, dialect);
|
|
33121
33332
|
const hasPromptGate = Boolean(handlerGroups[PI_PROMPT_BLOCKING_EVENT]);
|
|
33122
33333
|
const hasSettleGate = Boolean(handlerGroups[PI_SETTLE_BLOCKING_EVENT]);
|
|
33123
|
-
const
|
|
33334
|
+
const hasSessionStopGate = Boolean(handlerGroups[OMP_SESSION_STOP_BLOCKING_EVENT]);
|
|
33335
|
+
const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]) || hasPromptGate || hasSettleGate || hasSessionStopGate;
|
|
33124
33336
|
const lines = ["// Generated by rulesync. Do not edit manually."];
|
|
33125
33337
|
if (subscriptionLines.length === 0) {
|
|
33126
33338
|
lines.push("export default function () {}");
|
|
@@ -33147,6 +33359,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap, dialect =
|
|
|
33147
33359
|
lines.push(...PROMPT_GATE_HELPER_LINES);
|
|
33148
33360
|
lines.push("");
|
|
33149
33361
|
}
|
|
33362
|
+
if (hasSettleGate || hasSessionStopGate) {
|
|
33363
|
+
lines.push(...STOP_HOOK_BLOCK_HELPER_LINES);
|
|
33364
|
+
lines.push("");
|
|
33365
|
+
}
|
|
33150
33366
|
if (hasSettleGate) {
|
|
33151
33367
|
lines.push(...STOP_HOOK_HELPER_LINES);
|
|
33152
33368
|
lines.push("");
|
|
@@ -45504,18 +45720,21 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
45504
45720
|
};
|
|
45505
45721
|
//#endregion
|
|
45506
45722
|
//#region src/features/mcp/opencode-mcp.ts
|
|
45723
|
+
const OpencodeMcpV2DisabledSchema = z.optional(z.boolean());
|
|
45507
45724
|
const OpencodeMcpLocalServerSchema = z.looseObject({
|
|
45508
45725
|
type: z.literal("local"),
|
|
45509
45726
|
command: z.array(z.string()),
|
|
45510
45727
|
environment: z.optional(z.record(z.string(), z.string())),
|
|
45511
45728
|
enabled: z._default(z.boolean(), true),
|
|
45729
|
+
disabled: OpencodeMcpV2DisabledSchema,
|
|
45512
45730
|
cwd: z.optional(z.string())
|
|
45513
45731
|
});
|
|
45514
45732
|
const OpencodeMcpRemoteServerSchema = z.looseObject({
|
|
45515
45733
|
type: z.literal("remote"),
|
|
45516
45734
|
url: z.string(),
|
|
45517
45735
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
45518
|
-
enabled: z._default(z.boolean(), true)
|
|
45736
|
+
enabled: z._default(z.boolean(), true),
|
|
45737
|
+
disabled: OpencodeMcpV2DisabledSchema
|
|
45519
45738
|
});
|
|
45520
45739
|
const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
45521
45740
|
"timeout",
|
|
@@ -45527,7 +45746,7 @@ const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
|
45527
45746
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
45528
45747
|
...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
|
|
45529
45748
|
...OPENCODE_PASSTHROUGH_SERVER_FIELDS
|
|
45530
|
-
].filter((key) => key !== "enabled");
|
|
45749
|
+
].filter((key) => key !== "enabled" && key !== "disabled");
|
|
45531
45750
|
/**
|
|
45532
45751
|
* A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
|
|
45533
45752
|
* disabling a server another config layer defines. It is the third member of
|
|
@@ -45655,7 +45874,7 @@ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
|
|
|
45655
45874
|
function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
45656
45875
|
const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
|
|
45657
45876
|
if (dropped.length === 0) return;
|
|
45658
|
-
logger?.warn(`OpenCode MCP:
|
|
45877
|
+
logger?.warn(`OpenCode MCP: ${quoteValueForWarning(serverName)} declares no transport, so it is written as a toggle entry and ${dropped.toSorted().map(quoteValueForWarning).join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
|
|
45659
45878
|
}
|
|
45660
45879
|
/**
|
|
45661
45880
|
* Convert standard MCP format to OpenCode native format
|
|
@@ -45735,9 +45954,35 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45735
45954
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
45736
45955
|
static layout = OPENCODE_LAYOUT;
|
|
45737
45956
|
json;
|
|
45738
|
-
|
|
45957
|
+
/**
|
|
45958
|
+
* With a `logger`, the V2 values that lowering the content drops are warned
|
|
45959
|
+
* about (see {@link OpencodeMcp.lowerConfig}); `fromFile` passes one so the
|
|
45960
|
+
* warnings come from the lowering that builds the parsed view rather than
|
|
45961
|
+
* from an extra pass of their own. (`validate()`, which the parent
|
|
45962
|
+
* constructor calls before this view exists, lowers separately and silently.)
|
|
45963
|
+
*/
|
|
45964
|
+
constructor({ logger, ...params }) {
|
|
45739
45965
|
super(params);
|
|
45740
|
-
this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
45966
|
+
this.json = OpencodeConfigSchema.parse(this.constructor.lowerConfig(parse$1(this.fileContent || "{}"), logger));
|
|
45967
|
+
}
|
|
45968
|
+
/**
|
|
45969
|
+
* Lowers the OpenCode V2 `mcp` spellings of a parsed config for a layout
|
|
45970
|
+
* that reads them (see {@link lowerOpencodeV2Mcp}), so every reader — import,
|
|
45971
|
+
* the toggle lookup on generate, and the rules feature's `instructions`
|
|
45972
|
+
* write to the same file — accepts a V2-shaped `mcp`. Only the parsed view
|
|
45973
|
+
* is lowered; content written back keeps whatever `mcp` a feature that does
|
|
45974
|
+
* not own the key found there. With a `logger`, each V2 value the lowering
|
|
45975
|
+
* drops is warned about.
|
|
45976
|
+
*/
|
|
45977
|
+
static lowerConfig(json, logger) {
|
|
45978
|
+
if (!this.layout.readsV2ConfigSpellings || !isRecord$1(json) || json.mcp === void 0) return json;
|
|
45979
|
+
return {
|
|
45980
|
+
...json,
|
|
45981
|
+
mcp: lowerOpencodeV2Mcp(json.mcp, (path, reason) => {
|
|
45982
|
+
const quoted = quoteValueForWarning(path);
|
|
45983
|
+
logger?.warn(reason === "conflict" ? `OpenCode MCP: ${quoted} conflicts with a V1 value that takes precedence (the flat "mcp" entry of the same name, or "enabled"), so it is dropped on import.` : `OpenCode MCP: ${quoted} has no Rulesync equivalent, so it is dropped on import.`);
|
|
45984
|
+
})
|
|
45985
|
+
};
|
|
45741
45986
|
}
|
|
45742
45987
|
getJson() {
|
|
45743
45988
|
return this.json;
|
|
@@ -45754,7 +45999,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45754
45999
|
relativeFilePath: this.layout.jsonFileName
|
|
45755
46000
|
};
|
|
45756
46001
|
}
|
|
45757
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46002
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
45758
46003
|
const basePaths = this.getSettablePaths({ global });
|
|
45759
46004
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
45760
46005
|
let fileContent = null;
|
|
@@ -45776,7 +46021,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45776
46021
|
relativeDirPath: basePaths.relativeDirPath,
|
|
45777
46022
|
relativeFilePath,
|
|
45778
46023
|
fileContent: JSON.stringify(newJson, null, 2),
|
|
45779
|
-
validate
|
|
46024
|
+
validate,
|
|
46025
|
+
logger
|
|
45780
46026
|
});
|
|
45781
46027
|
}
|
|
45782
46028
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
@@ -45795,7 +46041,12 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45795
46041
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
45796
46042
|
replacement: "{env:$1}"
|
|
45797
46043
|
});
|
|
45798
|
-
const
|
|
46044
|
+
const existingJson = parse$1(fileContent || "{}");
|
|
46045
|
+
const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
|
|
46046
|
+
if (this.layout.readsV2ConfigSpellings && isRecord$1(existingJson)) {
|
|
46047
|
+
const containers = findOpencodeV2McpContainers(existingJson.mcp);
|
|
46048
|
+
if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
|
|
46049
|
+
}
|
|
45799
46050
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
45800
46051
|
return new this({
|
|
45801
46052
|
outputRoot,
|
|
@@ -45895,7 +46146,7 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
45895
46146
|
}
|
|
45896
46147
|
validate() {
|
|
45897
46148
|
const json = parseJsonc(this.fileContent || "{}");
|
|
45898
|
-
const result = OpencodeConfigSchema.safeParse(json);
|
|
46149
|
+
const result = OpencodeConfigSchema.safeParse(this.constructor.lowerConfig(json));
|
|
45899
46150
|
if (!result.success) return {
|
|
45900
46151
|
success: false,
|
|
45901
46152
|
error: result.error
|
|
@@ -55943,6 +56194,124 @@ function parseCodebuddySettings({ fileContent, relativePath }) {
|
|
|
55943
56194
|
}
|
|
55944
56195
|
}
|
|
55945
56196
|
//#endregion
|
|
56197
|
+
//#region src/features/permissions/codewhale-project-config.ts
|
|
56198
|
+
/**
|
|
56199
|
+
* The top-level key of the per-project `.codewhale/config.toml` overlay that
|
|
56200
|
+
* hides Codewhale's shell tools. The overlay only tightens: `false` disables
|
|
56201
|
+
* shell for the session, while `true` is ignored with a warning.
|
|
56202
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/docs/CONFIGURATION.md#per-project-overlay-485
|
|
56203
|
+
* @see https://github.com/Hmbown/Codewhale/blob/fead51eeea0fb6038ea3e30923751b1a1c765b25/crates/tui/src/lib.rs
|
|
56204
|
+
*/
|
|
56205
|
+
const CODEWHALE_ALLOW_SHELL_KEY = "allow_shell";
|
|
56206
|
+
const CODEWHALE_PROJECT_CONFIG_PATHS = {
|
|
56207
|
+
relativeDirPath: CODEWHALE_DIR,
|
|
56208
|
+
relativeFilePath: CODEWHALE_CONFIG_FILE_NAME
|
|
56209
|
+
};
|
|
56210
|
+
/** The project `.codewhale/config.toml` overlay, as written by the permissions feature. */
|
|
56211
|
+
var CodewhaleProjectConfigFile = class extends ToolFile {
|
|
56212
|
+
validate() {
|
|
56213
|
+
return {
|
|
56214
|
+
success: true,
|
|
56215
|
+
error: null
|
|
56216
|
+
};
|
|
56217
|
+
}
|
|
56218
|
+
};
|
|
56219
|
+
/**
|
|
56220
|
+
* Whether the canonical config denies every shell command: a `deny` on the
|
|
56221
|
+
* catch-all `*` (or `**`) pattern of `bash` or of the all-tools `*` category.
|
|
56222
|
+
* Codewhale decides by action before specificity, so such a deny already
|
|
56223
|
+
* blocks every command in global scope too, whatever `allow` rules sit beside
|
|
56224
|
+
* it.
|
|
56225
|
+
*/
|
|
56226
|
+
function deniesEveryShellCommand(permission) {
|
|
56227
|
+
return collectShellCommandRules(permission).rules.some(({ pattern, action }) => action === "deny" && isShellCatchAll(pattern));
|
|
56228
|
+
}
|
|
56229
|
+
/** Whether a canonical shell pattern names every command. */
|
|
56230
|
+
function isShellCatchAll(pattern) {
|
|
56231
|
+
const trimmed = pattern.trim();
|
|
56232
|
+
return trimmed === "*" || trimmed === "**";
|
|
56233
|
+
}
|
|
56234
|
+
function projectConfigFilePath(outputRoot) {
|
|
56235
|
+
return join(outputRoot, CODEWHALE_PROJECT_CONFIG_PATHS.relativeDirPath, CODEWHALE_PROJECT_CONFIG_PATHS.relativeFilePath);
|
|
56236
|
+
}
|
|
56237
|
+
function parseProjectConfig({ fileContent, filePath }) {
|
|
56238
|
+
return parseSharedConfig({
|
|
56239
|
+
format: "toml",
|
|
56240
|
+
fileContent,
|
|
56241
|
+
filePath,
|
|
56242
|
+
invalidRootPolicy: "error"
|
|
56243
|
+
});
|
|
56244
|
+
}
|
|
56245
|
+
/**
|
|
56246
|
+
* Whether an overlay disables shell (`allow_shell = false`). A file that
|
|
56247
|
+
* cannot be read as TOML is reported and treated as not disabling shell.
|
|
56248
|
+
*/
|
|
56249
|
+
function disablesShell({ fileContent, filePath, logger }) {
|
|
56250
|
+
try {
|
|
56251
|
+
return parseProjectConfig({
|
|
56252
|
+
fileContent,
|
|
56253
|
+
filePath
|
|
56254
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false;
|
|
56255
|
+
} catch (error) {
|
|
56256
|
+
logger?.warn(`Codewhale permissions: not reading "allow_shell" from ${filePath}, so a shell deny set there is not taken into account: ${formatError(error)}`);
|
|
56257
|
+
return false;
|
|
56258
|
+
}
|
|
56259
|
+
}
|
|
56260
|
+
/**
|
|
56261
|
+
* Whether the project overlay at `outputRoot` disables shell
|
|
56262
|
+
* (`allow_shell = false`), for import.
|
|
56263
|
+
*/
|
|
56264
|
+
async function readCodewhaleProjectShellDisabled({ outputRoot, logger }) {
|
|
56265
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56266
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
56267
|
+
if (fileContent === null) return false;
|
|
56268
|
+
return disablesShell({
|
|
56269
|
+
fileContent,
|
|
56270
|
+
filePath,
|
|
56271
|
+
logger
|
|
56272
|
+
});
|
|
56273
|
+
}
|
|
56274
|
+
/**
|
|
56275
|
+
* The project `.codewhale/config.toml` overlay carrying `allow_shell = false`
|
|
56276
|
+
* when the canonical config denies every shell command, or `null` when the
|
|
56277
|
+
* file needs no change.
|
|
56278
|
+
*
|
|
56279
|
+
* Only `allow_shell` is written; every other overlay key (`model`,
|
|
56280
|
+
* `approval_policy`, ...) is kept, and an existing file that is not valid TOML
|
|
56281
|
+
* is an error only when it has to be written. An existing
|
|
56282
|
+
* `allow_shell = false` is never removed: the overlay cannot widen access, and
|
|
56283
|
+
* a hand-written value cannot be told apart from one rulesync wrote, so it is
|
|
56284
|
+
* reported instead.
|
|
56285
|
+
*/
|
|
56286
|
+
async function createCodewhaleProjectConfigFile({ outputRoot = process.cwd(), config, logger }) {
|
|
56287
|
+
const filePath = projectConfigFilePath(outputRoot);
|
|
56288
|
+
const existingContent = await readFileContentOrNull(filePath);
|
|
56289
|
+
if (!deniesEveryShellCommand(config.permission)) {
|
|
56290
|
+
if (existingContent !== null && disablesShell({
|
|
56291
|
+
fileContent: existingContent,
|
|
56292
|
+
filePath,
|
|
56293
|
+
logger
|
|
56294
|
+
})) 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.`);
|
|
56295
|
+
return null;
|
|
56296
|
+
}
|
|
56297
|
+
if (parseProjectConfig({
|
|
56298
|
+
fileContent: existingContent ?? "",
|
|
56299
|
+
filePath
|
|
56300
|
+
})[CODEWHALE_ALLOW_SHELL_KEY] === false) return null;
|
|
56301
|
+
return new CodewhaleProjectConfigFile({
|
|
56302
|
+
outputRoot,
|
|
56303
|
+
...CODEWHALE_PROJECT_CONFIG_PATHS,
|
|
56304
|
+
fileContent: applySharedConfigPatch({
|
|
56305
|
+
fileKey: sharedConfigFileKey(CODEWHALE_PROJECT_CONFIG_PATHS),
|
|
56306
|
+
feature: "permissions",
|
|
56307
|
+
existingContent: existingContent ?? "",
|
|
56308
|
+
patch: { [CODEWHALE_ALLOW_SHELL_KEY]: false },
|
|
56309
|
+
filePath,
|
|
56310
|
+
logger
|
|
56311
|
+
})
|
|
56312
|
+
});
|
|
56313
|
+
}
|
|
56314
|
+
//#endregion
|
|
55946
56315
|
//#region src/features/permissions/codewhale-repo-law.ts
|
|
55947
56316
|
/**
|
|
55948
56317
|
* Project-scope Codewhale permissions: the enforced `protected_invariants` of
|
|
@@ -56093,7 +56462,8 @@ function collectRestrictions({ config, logger }) {
|
|
|
56093
56462
|
let skippedAllowCount = 0;
|
|
56094
56463
|
for (const [category, rules] of Object.entries(config.permission)) {
|
|
56095
56464
|
if (!REPO_LAW_CATEGORIES.includes(category)) {
|
|
56096
|
-
|
|
56465
|
+
const entries = Object.entries(rules);
|
|
56466
|
+
if ((category === "bash" && entries.some(([pattern, action]) => action === "deny" && isShellCatchAll(pattern)) ? entries.filter(([, action]) => action === "allow") : entries).length > 0) skippedCategories.push(category);
|
|
56097
56467
|
continue;
|
|
56098
56468
|
}
|
|
56099
56469
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -56819,17 +57189,32 @@ async function warnOnShadowedConstitution({ outputRoot, logger }) {
|
|
|
56819
57189
|
* Project scope manages the enforced `protected_invariants` of the repo
|
|
56820
57190
|
* constitution `.codewhale/constitution.json` (see `codewhale-repo-law.ts`):
|
|
56821
57191
|
* the project `.codewhale/config.toml` overlay cannot carry rules, but repo
|
|
56822
|
-
* law can hold writes to protected paths.
|
|
57192
|
+
* law can hold writes to protected paths. A deny on every shell command also
|
|
57193
|
+
* sets `allow_shell = false` in that overlay (see
|
|
57194
|
+
* `codewhale-project-config.ts`), which import reads back here.
|
|
56823
57195
|
*
|
|
56824
57196
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/CONFIGURATION.md
|
|
56825
57197
|
* @see https://github.com/Hmbown/Codewhale/blob/main/docs/AUTHORIZATION_ORDER.md
|
|
56826
57198
|
*/
|
|
56827
57199
|
var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
56828
|
-
|
|
57200
|
+
/**
|
|
57201
|
+
* Whether the project `.codewhale/config.toml` overlay disables shell
|
|
57202
|
+
* (`allow_shell = false`); read on import in project scope only.
|
|
57203
|
+
*/
|
|
57204
|
+
projectShellDisabled;
|
|
57205
|
+
constructor({ projectShellDisabled = false, ...params }) {
|
|
56829
57206
|
super({
|
|
56830
57207
|
...params,
|
|
56831
57208
|
fileContent: params.fileContent ?? ""
|
|
56832
57209
|
});
|
|
57210
|
+
this.projectShellDisabled = projectShellDisabled;
|
|
57211
|
+
}
|
|
57212
|
+
/**
|
|
57213
|
+
* The project `.codewhale/config.toml` overlay, where a deny on every shell
|
|
57214
|
+
* command becomes `allow_shell = false` beside the user's other overlay keys.
|
|
57215
|
+
*/
|
|
57216
|
+
static getExtraSharedWritePaths(options) {
|
|
57217
|
+
return options?.global ? [] : [{ ...CODEWHALE_PROJECT_CONFIG_PATHS }];
|
|
56833
57218
|
}
|
|
56834
57219
|
/**
|
|
56835
57220
|
* Codewhale's approval card writes to the global file, and the constitution
|
|
@@ -56866,7 +57251,11 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56866
57251
|
relativeFilePath: paths.relativeFilePath,
|
|
56867
57252
|
fileContent,
|
|
56868
57253
|
validate,
|
|
56869
|
-
global
|
|
57254
|
+
global,
|
|
57255
|
+
projectShellDisabled: global ? false : await readCodewhaleProjectShellDisabled({
|
|
57256
|
+
outputRoot,
|
|
57257
|
+
logger
|
|
57258
|
+
})
|
|
56870
57259
|
});
|
|
56871
57260
|
}
|
|
56872
57261
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
@@ -56928,11 +57317,15 @@ var CodewhalePermissions = class CodewhalePermissions extends ToolPermissions {
|
|
|
56928
57317
|
toRulesyncPermissions() {
|
|
56929
57318
|
const filePath = join(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
56930
57319
|
if (!this.global) {
|
|
56931
|
-
const
|
|
57320
|
+
const permission = codewhaleConstitutionToCanonical(parseCodewhaleConstitution({
|
|
56932
57321
|
fileContent: this.getFileContent(),
|
|
56933
57322
|
filePath
|
|
56934
|
-
});
|
|
56935
|
-
|
|
57323
|
+
}));
|
|
57324
|
+
if (this.projectShellDisabled) permission.bash = {
|
|
57325
|
+
...permission.bash,
|
|
57326
|
+
"*": "deny"
|
|
57327
|
+
};
|
|
57328
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
56936
57329
|
}
|
|
56937
57330
|
const { permission, nativeRules } = codewhaleRulesToCanonical(partitionRuleRecords(parsePermissionsDocument({
|
|
56938
57331
|
fileContent: this.getFileContent(),
|
|
@@ -71861,6 +72254,14 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
71861
72254
|
logger: this.logger,
|
|
71862
72255
|
global: this.global
|
|
71863
72256
|
});
|
|
72257
|
+
if (this.toolTarget === "codewhale" && !this.global) {
|
|
72258
|
+
const projectConfigFile = await createCodewhaleProjectConfigFile({
|
|
72259
|
+
outputRoot: this.outputRoot,
|
|
72260
|
+
config: effectivePermissions.getJson(),
|
|
72261
|
+
logger: this.logger
|
|
72262
|
+
});
|
|
72263
|
+
return projectConfigFile ? [toolPermissions, projectConfigFile] : [toolPermissions];
|
|
72264
|
+
}
|
|
71864
72265
|
if (this.toolTarget !== "codexcli") return [toolPermissions];
|
|
71865
72266
|
return [toolPermissions, createCodexcliBashRulesFile({
|
|
71866
72267
|
outputRoot: this.outputRoot,
|
|
@@ -101527,4 +101928,4 @@ async function importChecksCore(params) {
|
|
|
101527
101928
|
//#endregion
|
|
101528
101929
|
export { RulesyncHooks as $, removeFile as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
|
|
101529
101930
|
|
|
101530
|
-
//# sourceMappingURL=import-
|
|
101931
|
+
//# sourceMappingURL=import-5-Uennl1.js.map
|