Allow creating and refining Forgejo issues in idea (brainstorm) mode #243

Closed
opened 2026-11-09 15:10:12 +01:00 by omp-deck · 0 comments
Collaborator

Problem

Idea mode currently removes every Forgejo write tool from the session's tool set, so a brainstorm cannot end in a filed issue — the agent can only describe what it would have filed. The mode should allow a brainstorm to produce and refine issues, without becoming a general write mode.

Idea mode must allow exactly three Forgejo write tools:

  • forgejo_create_issue — unrestricted.
  • forgejo_create_issue_comment — issue numbers only; a pull-request number is denied.
  • forgejo_update_issue_description — issue numbers only, and only for issues opened by the agent's own Forgejo account.

Everything else stays as it is: all other Forgejo write tools remain unavailable, the filesystem stays read-only, and the three tools keep going through the existing "Forgejo write action requires approval" prompt (a user allow rule silences it; a user deny rule still wins). Any check that cannot be completed denies the call and names the reason. The idea-mode system prompt is not changed — the agent discovers the tools from the tool list, and the deny messages carry the rules.

Plan

1. Carry the issue author in DispatchApi.readIssue

apps/server/src/forgejo/dispatch-api.ts:

  • Add user?: { login?: string } | null; to interface RawIssue (currently number/html_url/title/state/pull_request/labels, around line 192). RawPull already declares exactly this field, so follow its shape.
  • Add /** Login of the user who opened the issue; empty when the instance did not report one. */ author: string; to interface IssueSummary (around line 109).
  • In readIssue's mapping (around line 343), add author: raw.user?.login ?? "", alongside title / state / isPull.
  • Update the readIssue doc line on the DispatchApi interface (line 30) to mention the author.

No other IssueSummary producer exists; consumers only read fields, so adding one is non-breaking. Verify with grep "IssueSummary" that no object literal outside this file constructs one — test fakes in dispatcher.test.ts and session-policy.test.ts build partial DispatchApis via as unknown as DispatchApi, so they need no change unless a fake literally constructs an IssueSummary, in which case add author: "someone" to it.

2. Idea-mode Forgejo policy module

New file apps/server/src/bridge/idea-mode-policy.ts. It is the single authority for which Forgejo writes idea mode permits, deliberately re-deciding the whole FORGEJO_MUTATING_TOOLS set rather than trusting that the tool was removed from the active set (the tool set is a visibility measure; this is the enforcement).

/** The only Forgejo write tools idea mode permits. */
export const IDEA_ALLOWED_FORGEJO_TOOLS: ReadonlySet<string> = new Set([
	"forgejo_create_issue",
	"forgejo_create_issue_comment",
	"forgejo_update_issue_description",
]);

export interface IdeaForgejoDeps {
	/** Repository the session's Forgejo tools are bound to; null when it has none. */
	repo: RepoRef | null;
	api: () => DispatchApi | null;
	agentUsername: () => string | null;
}

/**
 * Idea-mode verdict for one Forgejo tool call: "default" hands the call back
 * to the normal permission evaluation (rules, then the approval prompt).
 */
export async function decideIdeaModeForgejo(
	toolName: string,
	params: unknown,
	deps: IdeaForgejoDeps,
	signal?: AbortSignal,
): Promise<"default" | { deny: string }>

Behaviour, in order:

  1. if (!FORGEJO_MUTATING_TOOLS.has(toolName)) return "default"; — read tools and everything non-Forgejo are untouched. Import FORGEJO_MUTATING_TOOLS from ../forgejo/tools/index.ts.
  2. if (!IDEA_ALLOWED_FORGEJO_TOOLS.has(toolName)) return { deny: "not available in idea mode" };
  3. if (toolName === "forgejo_create_issue") return "default"; — no number, no authorship question.
  4. Extract the number: const number = typeof (params as {number?: unknown})?.number === "number" ? … : null; — on null return { deny: "idea mode could not read the issue number of this call" }.
  5. Resolve repo, api() and agentUsername(). If any is null/empty, return { deny: \idea mode could not verify #${number}: this session is not connected to a Forgejo repository` }`.
  6. const res = await api.readIssue(repo.owner, repo.repo, number, signal); — on !res.ok return { deny: \idea mode could not verify #${number}: Forgejo read failed (${res.status}: ${res.message})` }`. This covers 404 as well; the message shows the status so the agent can tell a wrong number from an outage.
  7. if (res.data.isPull) return { deny: \idea mode works on issues only; #${number} is a pull request` };`
  8. For forgejo_create_issue_comment, return "default" here.
  9. For forgejo_update_issue_description:
    • if (res.data.author === "") return { deny: \idea mode could not verify who opened #${number}` };`
    • if (res.data.author !== username) return { deny: \idea mode can only edit the description of issues opened by ${username}; #${number} was opened by ${res.data.author}` };`
    • otherwise "default".

ApiResult's failure shape is { ok: false, status, message } (see apps/server/src/forgejo/client.ts); confirm the exact field names there before writing step 6.

3. Enforce the policy inside PermissionFilter

apps/server/src/bridge/permission-filter.ts is already on the call path for the Forgejo custom tools (they are wrapped by wrapToolWithFilterRef) and already holds cwd plus forgejoApi: () => DispatchApi | null. This is where the policy runs — not in idea-mode-guard.ts, which has no API handle and would race the filter's approval prompt with a second verdict.

  • Extend the constructor options with ideaMode?: () => boolean (default () => false) and forgejoRepo?: RepoRef | null (default null), stored as private fields next to forgejoApi (lines 192-207).
  • In evaluatePermission, insert the idea-mode branch immediately after the trigger-policy block (which ends at line 255) and before the rule.action === "allow" lookup, so the restriction cannot be loosened by an allow rule, while the explicit user deny rule at lines 247-250 still wins:
if (this.ideaMode()) {
	const decision = await decideIdeaModeForgejo(
		toolName,
		params,
		{ repo: this.forgejoRepo, api: this.forgejoApi, agentUsername: () => readForgejoConfig()?.agentUsername ?? null },
		signal,
	);
	if (decision !== "default") return { deny: `Tool call denied for ${toolName}: ${decision.deny}` };
}

The Tool call denied for <tool>: <reason> prefix matches the trigger-policy branch at line 254. Import readForgejoConfig from ../forgejo/config.ts; it returns null unless url, token and agentUsername are all stored (config.ts:94-97), so a non-null config always has a username.

Trigger sessions cannot enter idea mode (in-process.ts:2109 throws), so the two branches never both fire; no interaction to handle.

4. Unblock the three tools in the idea-mode tool set

apps/server/src/bridge/idea-mode-bridge.ts:

  • In IDEA_BLOCKED_TOOLS (lines 29-43), change the Forgejo spread to skip the permitted three:
    ...Object.fromEntries([...FORGEJO_MUTATING_TOOLS].filter((n) => !IDEA_ALLOWED_FORGEJO_TOOLS.has(n)).map((k) => [k, true as const])),
    Import IDEA_ALLOWED_FORGEJO_TOOLS from ./idea-mode-policy.ts.
  • Rewrite the comment above it (lines 40-41, which currently explains that name-blocking is used because default-ask is overridable by an allow rule) to state the new division: issue creation, issue comments and description updates stay registered and are policed per call by idea-mode-policy.ts; every other Forgejo write tool is removed from the tool set here.
  • Replace the duplicated filter expression at lines 127 and 181 (this.session.getAllToolNames().filter((t) => !(t in IDEA_BLOCKED_TOOLS))) with one exported helper so the rule is testable without an SDK session:
/** The tool set idea mode runs with: everything registered minus {@link IDEA_BLOCKED_TOOLS}. */
export function ideaModeToolNames(all: readonly string[]): string[] {
	return all.filter((t) => !(t in IDEA_BLOCKED_TOOLS));
}

Both enter() and restore() call it, so the restored-after-restart tool set matches the fresh one automatically.

IDEA_MODE_SYSTEM_PROMPT (lines 45-59) is left unchanged.

5. Wire the session's idea state and repo into the filter

apps/server/src/bridge/in-process.ts:

  • Add a forgejoRepo: RepoRef | null parameter to attach(...) (signature at lines 929-939), passed at both call sites from the already-destructured forgejoRepo (:298 in createSession, :454 in the resume path). Import type RepoRef from ../forgejo/repo.ts.
  • Construct the filter (line 982) as:
    new PermissionFilter({ sessionId, cwd, trigger, workspaceId: placement?.workspaceId ?? undefined, forgejoRepo, ideaMode: () => ideaBridgeRef?.current?.isEnabled() === true }).
    The predicate is evaluated per call, so it is safe that ideaBridgeRef.current is only assigned at line 996, after the filter is built.

No change to idea-mode-guard.ts: its write/edit/ast_edit/eval contract is unrelated, and write to xd:// already reaches the Forgejo tools through the same PermissionFilter.

Critical files & anchors

  • apps/server/src/bridge/permission-filter.ts — evaluatePermission (lines 239-299); the ordering deny-rule → trigger policy → allow-rule → defaultAsk → prompt is what makes the insertion point load-bearing.
  • apps/server/src/bridge/idea-mode-bridge.ts — IDEA_BLOCKED_TOOLS (29-43) and the two identical filter expressions at 127 and 181.
  • apps/server/src/forgejo/dispatch-api.ts — IssueSummary (109-114), RawIssue (192-199), readIssue mapping (339-353).
  • apps/server/src/bridge/in-process.ts — attach signature (929-939), filter construction (982), idea bridge creation (992-996), forgejoRepo destructuring (298, 454).
  • apps/server/src/bridge/permission-filter.test.ts — harness() (65-98) is the pattern for the new integration tests: real filter, real temp SQLite, plain tool objects.

Verification

All commands run from apps/server.

  1. Policy unit tests — new apps/server/src/bridge/idea-mode-policy.test.ts, driving decideIdeaModeForgejo against a fake DispatchApi built as { async readIssue() {…} } as unknown as DispatchApi (same trick as session-policy.test.ts:30-40). Each case states input → expected output:
    • ("forgejo_create_issue", { title: "x" }) → "default" (no readIssue call made).
    • ("forgejo_create_issue_comment", { number: 7 }) where #7 is { isPull: false, author: "someone" } → "default".
    • same call where #7 is { isPull: true } → deny containing #7 is a pull request.
    • ("forgejo_update_issue_description", { number: 7 }) with author: "deckbot" and agentUsername: () => "deckbot" → "default".
    • same with author: "human" → deny containing was opened by human.
    • same with author: "" → deny containing could not verify who opened #7.
    • readIssue failing with { ok: false, status: 503, message: "boom" } → deny containing 503.
    • repo: null → deny containing not connected to a Forgejo repository, with no readIssue call.
    • ("forgejo_close_issue", { number: 7 }) → deny not available in idea mode.
    • ("forgejo_read_issue", { number: 7 }) → "default".
    • ("forgejo_update_issue_description", {}) (no number) → deny containing could not read the issue number.
  2. Filter integration tests — added to apps/server/src/bridge/permission-filter.test.ts in a new describe("idea mode"), using harness() extended to accept the new constructor options:
    • ideaMode: () => true, forgejo_create_issue → a prompt appears with ruleId default:forgejo_create_issue, approving runs the tool.
    • ideaMode: () => true + createPermission({ toolPattern: "^forgejo_create_issue$", action: "allow" }) → runs with no prompt.
    • ideaMode: () => true + a foreign-author issue for forgejo_update_issue_description → denied, no prompt emitted, even with an ^forgejo_ allow rule present (proves the branch sits above the allow lookup).
    • createPermission({ toolPattern: "^forgejo_create_issue$", action: "deny" }) + ideaMode: () => true → denied with the rule's message (deny rule still wins).
    • ideaMode: () => false with a foreign-author issue → prompts as before (proves the policy is idea-mode-only).
  3. Tool-set test — new apps/server/src/bridge/idea-mode-bridge.test.ts asserting ideaModeToolNames([...FORGEJO_TOOL_NAMES, "bash", "git_commit", "read"]) contains the three permitted Forgejo tools and read, and contains neither bash, git_commit, forgejo_close_issue, nor forgejo_post_review.
  4. Suite and types — bun test and bun run typecheck in apps/server; then bun run typecheck at the repo root for the workspace.
  5. End-to-end in the deck (needs a running deck whose Forgejo integration is configured, and a session whose workspace origin is on that instance): start a session there, switch to idea mode (Shift+Tab or /idea on), ask the agent to file an issue about the brainstorm. Expected: forgejo_create_issue appears as an approval prompt; approving creates the issue on Forgejo. Then ask it to rewrite the description of an issue opened by a human. Expected: no prompt, and the agent reports the deny idea mode can only edit the description of issues opened by <agent>; #N was opened by <human>. Finally ask it to comment on a pull request number. Expected: deny containing is a pull request.

Assumptions & contingencies

  • The Forgejo API returns the opener of an issue as user.login on GET /repos/{owner}/{repo}/issues/{n}. RawPull in the same file already relies on user.login for pull requests. If the issue payload turns out to omit it, step 9's empty-author branch already denies safely; do not fall back to a second API call.
  • forgejoRepo is non-null exactly when the session has Forgejo tools at all, so a null repo in the policy means the tools were never registered and the deny is unreachable in practice; keep it as the fail-closed path rather than an assertion.
## Problem Idea mode currently removes every Forgejo write tool from the session's tool set, so a brainstorm cannot end in a filed issue — the agent can only describe what it would have filed. The mode should allow a brainstorm to produce and refine issues, without becoming a general write mode. Idea mode must allow exactly three Forgejo write tools: - `forgejo_create_issue` — unrestricted. - `forgejo_create_issue_comment` — issue numbers only; a pull-request number is denied. - `forgejo_update_issue_description` — issue numbers only, **and** only for issues opened by the agent's own Forgejo account. Everything else stays as it is: all other Forgejo write tools remain unavailable, the filesystem stays read-only, and the three tools keep going through the existing `"Forgejo write action requires approval"` prompt (a user *allow* rule silences it; a user *deny* rule still wins). Any check that cannot be completed denies the call and names the reason. The idea-mode system prompt is **not** changed — the agent discovers the tools from the tool list, and the deny messages carry the rules. ## Plan ### 1. Carry the issue author in `DispatchApi.readIssue` `apps/server/src/forgejo/dispatch-api.ts`: - Add `user?: { login?: string } | null;` to `interface RawIssue` (currently `number/html_url/title/state/pull_request/labels`, around line 192). `RawPull` already declares exactly this field, so follow its shape. - Add `/** Login of the user who opened the issue; empty when the instance did not report one. */ author: string;` to `interface IssueSummary` (around line 109). - In `readIssue`'s mapping (around line 343), add `author: raw.user?.login ?? "",` alongside `title` / `state` / `isPull`. - Update the `readIssue` doc line on the `DispatchApi` interface (line 30) to mention the author. No other `IssueSummary` producer exists; consumers only read fields, so adding one is non-breaking. Verify with `grep "IssueSummary"` that no object literal outside this file constructs one — test fakes in `dispatcher.test.ts` and `session-policy.test.ts` build partial `DispatchApi`s via `as unknown as DispatchApi`, so they need no change unless a fake literally constructs an `IssueSummary`, in which case add `author: "someone"` to it. ### 2. Idea-mode Forgejo policy module New file `apps/server/src/bridge/idea-mode-policy.ts`. It is the single authority for which Forgejo writes idea mode permits, deliberately re-deciding the whole `FORGEJO_MUTATING_TOOLS` set rather than trusting that the tool was removed from the active set (the tool set is a visibility measure; this is the enforcement). ```ts /** The only Forgejo write tools idea mode permits. */ export const IDEA_ALLOWED_FORGEJO_TOOLS: ReadonlySet<string> = new Set([ "forgejo_create_issue", "forgejo_create_issue_comment", "forgejo_update_issue_description", ]); export interface IdeaForgejoDeps { /** Repository the session's Forgejo tools are bound to; null when it has none. */ repo: RepoRef | null; api: () => DispatchApi | null; agentUsername: () => string | null; } /** * Idea-mode verdict for one Forgejo tool call: "default" hands the call back * to the normal permission evaluation (rules, then the approval prompt). */ export async function decideIdeaModeForgejo( toolName: string, params: unknown, deps: IdeaForgejoDeps, signal?: AbortSignal, ): Promise<"default" | { deny: string }> ``` Behaviour, in order: 1. `if (!FORGEJO_MUTATING_TOOLS.has(toolName)) return "default";` — read tools and everything non-Forgejo are untouched. Import `FORGEJO_MUTATING_TOOLS` from `../forgejo/tools/index.ts`. 2. `if (!IDEA_ALLOWED_FORGEJO_TOOLS.has(toolName)) return { deny: "not available in idea mode" };` 3. `if (toolName === "forgejo_create_issue") return "default";` — no number, no authorship question. 4. Extract the number: `const number = typeof (params as {number?: unknown})?.number === "number" ? … : null;` — on `null` return `{ deny: "idea mode could not read the issue number of this call" }`. 5. Resolve `repo`, `api()` and `agentUsername()`. If any is null/empty, return `{ deny: \`idea mode could not verify #${number}: this session is not connected to a Forgejo repository\` }`. 6. `const res = await api.readIssue(repo.owner, repo.repo, number, signal);` — on `!res.ok` return `{ deny: \`idea mode could not verify #${number}: Forgejo read failed (${res.status}: ${res.message})\` }`. This covers 404 as well; the message shows the status so the agent can tell a wrong number from an outage. 7. `if (res.data.isPull) return { deny: \`idea mode works on issues only; #${number} is a pull request\` };` 8. For `forgejo_create_issue_comment`, return `"default"` here. 9. For `forgejo_update_issue_description`: - `if (res.data.author === "") return { deny: \`idea mode could not verify who opened #${number}\` };` - `if (res.data.author !== username) return { deny: \`idea mode can only edit the description of issues opened by ${username}; #${number} was opened by ${res.data.author}\` };` - otherwise `"default"`. `ApiResult`'s failure shape is `{ ok: false, status, message }` (see `apps/server/src/forgejo/client.ts`); confirm the exact field names there before writing step 6. ### 3. Enforce the policy inside `PermissionFilter` `apps/server/src/bridge/permission-filter.ts` is already on the call path for the Forgejo custom tools (they are wrapped by `wrapToolWithFilterRef`) and already holds `cwd` plus `forgejoApi: () => DispatchApi | null`. This is where the policy runs — not in `idea-mode-guard.ts`, which has no API handle and would race the filter's approval prompt with a second verdict. - Extend the constructor options with `ideaMode?: () => boolean` (default `() => false`) and `forgejoRepo?: RepoRef | null` (default `null`), stored as private fields next to `forgejoApi` (lines 192-207). - In `evaluatePermission`, insert the idea-mode branch immediately **after** the trigger-policy block (which ends at line 255) and **before** the `rule.action === "allow"` lookup, so the restriction cannot be loosened by an allow rule, while the explicit user *deny* rule at lines 247-250 still wins: ```ts if (this.ideaMode()) { const decision = await decideIdeaModeForgejo( toolName, params, { repo: this.forgejoRepo, api: this.forgejoApi, agentUsername: () => readForgejoConfig()?.agentUsername ?? null }, signal, ); if (decision !== "default") return { deny: `Tool call denied for ${toolName}: ${decision.deny}` }; } ``` The `Tool call denied for <tool>: <reason>` prefix matches the trigger-policy branch at line 254. Import `readForgejoConfig` from `../forgejo/config.ts`; it returns `null` unless url, token and `agentUsername` are all stored (`config.ts:94-97`), so a non-null config always has a username. Trigger sessions cannot enter idea mode (`in-process.ts:2109` throws), so the two branches never both fire; no interaction to handle. ### 4. Unblock the three tools in the idea-mode tool set `apps/server/src/bridge/idea-mode-bridge.ts`: - In `IDEA_BLOCKED_TOOLS` (lines 29-43), change the Forgejo spread to skip the permitted three: `...Object.fromEntries([...FORGEJO_MUTATING_TOOLS].filter((n) => !IDEA_ALLOWED_FORGEJO_TOOLS.has(n)).map((k) => [k, true as const])),` Import `IDEA_ALLOWED_FORGEJO_TOOLS` from `./idea-mode-policy.ts`. - Rewrite the comment above it (lines 40-41, which currently explains that name-blocking is used *because* default-ask is overridable by an allow rule) to state the new division: issue creation, issue comments and description updates stay registered and are policed per call by `idea-mode-policy.ts`; every other Forgejo write tool is removed from the tool set here. - Replace the duplicated filter expression at lines 127 and 181 (`this.session.getAllToolNames().filter((t) => !(t in IDEA_BLOCKED_TOOLS))`) with one exported helper so the rule is testable without an SDK session: ```ts /** The tool set idea mode runs with: everything registered minus {@link IDEA_BLOCKED_TOOLS}. */ export function ideaModeToolNames(all: readonly string[]): string[] { return all.filter((t) => !(t in IDEA_BLOCKED_TOOLS)); } ``` Both `enter()` and `restore()` call it, so the restored-after-restart tool set matches the fresh one automatically. `IDEA_MODE_SYSTEM_PROMPT` (lines 45-59) is left unchanged. ### 5. Wire the session's idea state and repo into the filter `apps/server/src/bridge/in-process.ts`: - Add a `forgejoRepo: RepoRef | null` parameter to `attach(...)` (signature at lines 929-939), passed at both call sites from the already-destructured `forgejoRepo` (`:298` in `createSession`, `:454` in the resume path). Import `type RepoRef` from `../forgejo/repo.ts`. - Construct the filter (line 982) as: `new PermissionFilter({ sessionId, cwd, trigger, workspaceId: placement?.workspaceId ?? undefined, forgejoRepo, ideaMode: () => ideaBridgeRef?.current?.isEnabled() === true })`. The predicate is evaluated per call, so it is safe that `ideaBridgeRef.current` is only assigned at line 996, after the filter is built. No change to `idea-mode-guard.ts`: its `write`/`edit`/`ast_edit`/`eval` contract is unrelated, and `write` to `xd://` already reaches the Forgejo tools through the same `PermissionFilter`. ## Critical files & anchors - `apps/server/src/bridge/permission-filter.ts` — `evaluatePermission` (lines 239-299); the ordering deny-rule → trigger policy → allow-rule → `defaultAsk` → prompt is what makes the insertion point load-bearing. - `apps/server/src/bridge/idea-mode-bridge.ts` — `IDEA_BLOCKED_TOOLS` (29-43) and the two identical filter expressions at 127 and 181. - `apps/server/src/forgejo/dispatch-api.ts` — `IssueSummary` (109-114), `RawIssue` (192-199), `readIssue` mapping (339-353). - `apps/server/src/bridge/in-process.ts` — `attach` signature (929-939), filter construction (982), idea bridge creation (992-996), `forgejoRepo` destructuring (298, 454). - `apps/server/src/bridge/permission-filter.test.ts` — `harness()` (65-98) is the pattern for the new integration tests: real filter, real temp SQLite, plain tool objects. ## Verification All commands run from `apps/server`. 1. **Policy unit tests** — new `apps/server/src/bridge/idea-mode-policy.test.ts`, driving `decideIdeaModeForgejo` against a fake `DispatchApi` built as `{ async readIssue() {…} } as unknown as DispatchApi` (same trick as `session-policy.test.ts:30-40`). Each case states input → expected output: - `("forgejo_create_issue", { title: "x" })` → `"default"` (no `readIssue` call made). - `("forgejo_create_issue_comment", { number: 7 })` where #7 is `{ isPull: false, author: "someone" }` → `"default"`. - same call where #7 is `{ isPull: true }` → deny containing `#7 is a pull request`. - `("forgejo_update_issue_description", { number: 7 })` with `author: "deckbot"` and `agentUsername: () => "deckbot"` → `"default"`. - same with `author: "human"` → deny containing `was opened by human`. - same with `author: ""` → deny containing `could not verify who opened #7`. - `readIssue` failing with `{ ok: false, status: 503, message: "boom" }` → deny containing `503`. - `repo: null` → deny containing `not connected to a Forgejo repository`, with no `readIssue` call. - `("forgejo_close_issue", { number: 7 })` → deny `not available in idea mode`. - `("forgejo_read_issue", { number: 7 })` → `"default"`. - `("forgejo_update_issue_description", {})` (no number) → deny containing `could not read the issue number`. 2. **Filter integration tests** — added to `apps/server/src/bridge/permission-filter.test.ts` in a new `describe("idea mode")`, using `harness()` extended to accept the new constructor options: - `ideaMode: () => true`, `forgejo_create_issue` → a prompt appears with ruleId `default:forgejo_create_issue`, approving runs the tool. - `ideaMode: () => true` + `createPermission({ toolPattern: "^forgejo_create_issue$", action: "allow" })` → runs with no prompt. - `ideaMode: () => true` + a foreign-author issue for `forgejo_update_issue_description` → denied, **no prompt emitted**, even with an `^forgejo_` allow rule present (proves the branch sits above the allow lookup). - `createPermission({ toolPattern: "^forgejo_create_issue$", action: "deny" })` + `ideaMode: () => true` → denied with the rule's message (deny rule still wins). - `ideaMode: () => false` with a foreign-author issue → prompts as before (proves the policy is idea-mode-only). 3. **Tool-set test** — new `apps/server/src/bridge/idea-mode-bridge.test.ts` asserting `ideaModeToolNames([...FORGEJO_TOOL_NAMES, "bash", "git_commit", "read"])` contains the three permitted Forgejo tools and `read`, and contains neither `bash`, `git_commit`, `forgejo_close_issue`, nor `forgejo_post_review`. 4. **Suite and types** — `bun test` and `bun run typecheck` in `apps/server`; then `bun run typecheck` at the repo root for the workspace. 5. **End-to-end in the deck** (needs a running deck whose Forgejo integration is configured, and a session whose workspace origin is on that instance): start a session there, switch to idea mode (Shift+Tab or `/idea on`), ask the agent to file an issue about the brainstorm. Expected: `forgejo_create_issue` appears as an approval prompt; approving creates the issue on Forgejo. Then ask it to rewrite the description of an issue opened by a human. Expected: no prompt, and the agent reports the deny `idea mode can only edit the description of issues opened by <agent>; #N was opened by <human>`. Finally ask it to comment on a pull request number. Expected: deny containing `is a pull request`. ## Assumptions & contingencies - The Forgejo API returns the opener of an issue as `user.login` on `GET /repos/{owner}/{repo}/issues/{n}`. `RawPull` in the same file already relies on `user.login` for pull requests. If the issue payload turns out to omit it, step 9's empty-author branch already denies safely; do not fall back to a second API call. - `forgejoRepo` is non-null exactly when the session has Forgejo tools at all, so a null repo in the policy means the tools were never registered and the deny is unreachable in practice; keep it as the fail-closed path rather than an assertion.
lucas closed this issue 2026-11-09 15:58:55 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck#243
No description provided.