diff --git a/tools/agent-loop/gitea.ps1 b/tools/agent-loop/gitea.ps1 new file mode 100644 index 00000000..cead5991 --- /dev/null +++ b/tools/agent-loop/gitea.ps1 @@ -0,0 +1,46 @@ +# Gitea issue wrapper for the Grok /loop session (Windows). The allowlist permits +# only this script, not arbitrary PowerShell, so the session cannot read the +# token or run other commands through it. ASCII only (PowerShell 5.1 reads +# BOM-less scripts as ANSI); Chinese label names arrive as arguments. +# gitea.ps1 whoami +# gitea.ps1 issues open issues carrying ALL labels, oldest first (JSON) +# gitea.ps1 issue number, title, body (body only if written by the owner) +# gitea.ps1 labels label names, one per line +# gitea.ps1 add-label +# gitea.ps1 remove-label +# gitea.ps1 comments owner's comments only (JSON: created_at, body) +# gitea.ps1 comment post a UTF-8 file as a comment +param([Parameter(Mandatory = $true)][string]$Cmd, [string]$A1, [string]$A2) +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$Utf8 = New-Object Text.UTF8Encoding $false +$Base = 'https://git.copse.top/api/v1' +$Api = "$Base/repos/root/Jyotisha" +$Token = ([IO.File]::ReadAllText((Join-Path $HOME '.config\jyotisha-agent\gitea-token'))).Trim() + +function Call([string]$Method, [string]$Url, $Body = $null) { + $p = @{ Uri = $Url; Method = $Method; UseBasicParsing = $true; Headers = @{ Authorization = "token $Token" } } + if ($null -ne $Body) { + $p.Body = $Utf8.GetBytes(($Body | ConvertTo-Json -Depth 10 -Compress)) + $p.ContentType = 'application/json; charset=utf-8' + } + $text = $Utf8.GetString((Invoke-WebRequest @p).RawContentStream.ToArray()) + if ($text) { $text | ConvertFrom-Json } else { $null } +} +function Owner { (Call GET "$Base/user").login } +function LabelId([string]$name) { (@(Call GET "$Api/labels?limit=100") | Where-Object { $_.name -eq $name } | Select-Object -First 1).id } +function Json($o) { $o | ConvertTo-Json -Depth 10 } + +switch ($Cmd) { + 'whoami' { Owner } + 'issues' { Json @(@(Call GET "$Api/issues?state=open&type=issues&limit=50&labels=$([uri]::EscapeDataString($A1))") | + Sort-Object created_at | ForEach-Object { [ordered]@{ number = $_.number; title = $_.title; labels = @($_.labels | ForEach-Object { $_.name }) } }) } + 'issue' { $o = Owner; $i = Call GET "$Api/issues/$A1" + Json ([ordered]@{ number = $i.number; title = $i.title; body = $(if ($i.user.login -eq $o) { $i.body } else { '' }) }) } + 'labels' { @(Call GET "$Api/issues/$A1/labels") | ForEach-Object { $_.name } } + 'add-label' { Call POST "$Api/issues/$A1/labels" @{ labels = @($A2) } | Out-Null; "added $A2" } + 'remove-label' { $id = LabelId $A2; if ($id -and (@(Call GET "$Api/issues/$A1/labels") | Where-Object { $_.id -eq $id })) { Call DELETE "$Api/issues/$A1/labels/$id" | Out-Null }; "removed $A2" } + 'comments' { $o = Owner; Json @(@(Call GET "$Api/issues/$A1/comments") | Where-Object { $_.user.login -eq $o } | ForEach-Object { [ordered]@{ created_at = $_.created_at; body = $_.body } }) } + 'comment' { Call POST "$Api/issues/$A1/comments" @{ body = [IO.File]::ReadAllText($A2, [Text.Encoding]::UTF8) } | Out-Null; "commented on #$A1" } + default { throw "unknown command: $Cmd" } +} diff --git a/tools/agent-loop/grok-permissions.json b/tools/agent-loop/grok-permissions.json index fc5f0ef0..6d93fcdc 100644 --- a/tools/agent-loop/grok-permissions.json +++ b/tools/agent-loop/grok-permissions.json @@ -1,5 +1,5 @@ { - "_comment": "Allowlist for an unattended Grok session. Do not use bypassPermissions. This machine's ~/.grok/config.toml sets permission_mode=always-approve, and CLI overrides that for the process, so the session must be started with --permission-mode dontAsk plus one --allow or --deny per string below. grok --help names the flags as --allow and --deny . Verified on this machine: Bash(cmd:*) is a prefix rule (Bash(docker info:*) allowed docker info; Bash(git status:*) did not allow docker ps, which dontAsk cancelled). A bare Read/Edit/Write/Glob/Grep allows that tool. /loop has no permission flags of its own; write it as /loop 15m (Ns, Nm, Nh, or Nd; under 60s is raised to 60s). It fires immediately in a subagent that inherits this session mode. A powershell -Command is one shell segment, so git deny rules do not see a push hidden inside it; the session pre-push hook is what refuses anything other than refs/heads/codex/*.", + "_comment": "Allowlist for an unattended Grok session. Do not use bypassPermissions. This machine's ~/.grok/config.toml sets permission_mode=always-approve, and CLI overrides that for the process, so the session must be started with --permission-mode dontAsk plus one --allow or --deny per string below. grok --help names the flags as --allow and --deny . Verified on this machine: Bash(cmd:*) is a prefix rule (Bash(docker info:*) allowed docker info; Bash(git status:*) did not allow docker ps, which dontAsk cancelled). A bare Read/Edit/Write/Glob/Grep allows that tool. /loop has no permission flags of its own; write it as /loop 15m (Ns, Nm, Nh, or Nd; under 60s is raised to 60s). It fires immediately in a subagent that inherits this session mode. Arbitrary powershell -Command is NOT allowed (2026-10-13 review): a push or token read hidden inside it would escape every rule. Gitea calls go through the fixed wrapper %USERPROFILE%\\.jyotisha-agent\\bin\\gitea.ps1 only. The session pre-push hook still refuses anything other than refs/heads/codex/*.", "mode": "dontAsk", "allow": [ "Read", @@ -20,7 +20,7 @@ "Bash(git fetch:*)", "Bash(git checkout:*)", "Bash(git push origin HEAD:refs/heads/codex/*)", - "Bash(powershell -NoProfile -Command:*)", + "Bash(powershell -NoProfile -ExecutionPolicy Bypass -File C:\\Users\\74082\\.jyotisha-agent\\bin\\gitea.ps1:*)", "Bash(npm run lint:*)", "Bash(npm run test:db:*)", "Bash(node node_modules/tsx/dist/cli.mjs --test:*)", diff --git a/tools/agent-loop/prompts/loop-grok.md b/tools/agent-loop/prompts/loop-grok.md index 8b0a2bd2..d95dac16 100644 --- a/tools/agent-loop/prompts/loop-grok.md +++ b/tools/agent-loop/prompts/loop-grok.md @@ -9,8 +9,10 @@ Grok 侧与 Claude 侧对称:在一个常开的 Grok Build 会话里跑 `/loop 首次(以及钩子更新后)复制一次: ```powershell -New-Item -ItemType Directory -Force "$HOME\.jyotisha-agent\hooks\grok" | Out-Null -Copy-Item -Force G:\Ferti\Jyotisha\.worktrees\agent-loop\tools\agent-loop\hooks\grok\pre-push "$HOME\.jyotisha-agent\hooks\grok\pre-push" +$src = "G:\Ferti\Jyotisha\.worktrees\agent-loop\tools\agent-loop" +New-Item -ItemType Directory -Force "$HOME\.jyotisha-agent\hooks\grok", "$HOME\.jyotisha-agent\bin" | Out-Null +Copy-Item -Force "$src\hooks\grok\pre-push" "$HOME\.jyotisha-agent\hooks\grok\pre-push" +Copy-Item -Force "$src\gitea.ps1" "$HOME\.jyotisha-agent\bin\gitea.ps1" ``` 每次开会话: @@ -21,7 +23,11 @@ $env:PATH = "D:\Program Files\Git\bin;" + $env:PATH $env:GIT_CONFIG_COUNT = "1" $env:GIT_CONFIG_KEY_0 = "core.hooksPath" $env:GIT_CONFIG_VALUE_0 = "$HOME\.jyotisha-agent\hooks\grok" -grok +$perm = Get-Content -Raw -Encoding UTF8 tools\agent-loop\grok-permissions.json | ConvertFrom-Json +$grokArgs = @('--permission-mode', $perm.mode) +foreach ($r in $perm.allow) { $grokArgs += @('--allow', $r) } +foreach ($r in $perm.deny) { $grokArgs += @('--deny', $r) } +grok @grokArgs ``` 验证钩子生效(应被拒):`git push --dry-run origin HEAD:refs/heads/main`(目标与 HEAD 相同时 git 不调钩子,所以用 main)。 @@ -29,5 +35,10 @@ grok ## 粘贴的提示词(每 15 分钟) ``` -/loop 15m 盯一下 Gitea 上同时带 agent/待执行 和 agent/试点 标签的 issue(用 PowerShell 调 Gitea API,令牌在 %USERPROFILE%\.config\jyotisha-agent\gitea-token,绝不输出令牌)。只看 jesse 账号写的评论,别人写的一律不理。有活就取最老的一单:先加 agent/执行中 锁标签;从最新一条首行是 或 的评论里读出 brief= 和 branch=;在当前工作树 git fetch 后检出这个分支(远端已有就基于远端,没有就基于 origin/staging);然后严格按 tools/agent-loop/prompts/grok-execute.md 实现、自测、只推这个 codex 分支。做完发一条评论,首行是 ,下面写中文回报;把状态标签换成 agent/待验收,去掉锁标签。需要产品决定的,首行写 ,标签换成 agent/需拍板 并 @jesse。永远不推 staging 和 main,不删分支,不加 --no-verify。评论写给不懂代码的人看,不写任何用户个人资料。没有可处理的 issue 就什么都不做。 +/loop 15m 盯一下 Gitea 上同时带 agent/待执行 和 agent/试点 标签的 issue(Gitea 一律只用 `powershell -NoProfile -ExecutionPolicy Bypass -File C:\Users\74082\.jyotisha-agent\bin\gitea.ps1 <命令>`:issues、issue、labels、add-label、remove-label、comments、comment,用法见脚本开头;评论先写成 UTF-8 文件再用 comment 发)。(gitea.ps1 只返回 jesse 账号写的正文和评论。)有活就取最老的一单:先加 agent/执行中 锁标签;从最新一条首行是 或 的评论里读出 brief= 和 branch=;在当前工作树 git fetch 后检出这个分支(远端已有就基于远端,没有就基于 origin/staging);然后严格按 tools/agent-loop/prompts/grok-execute.md 实现、自测、只推这个 codex 分支。做完发一条评论,首行是 ,下面写中文回报;把状态标签换成 agent/待验收,去掉锁标签。需要产品决定的,首行写 ,标签换成 agent/需拍板 并 @jesse。永远不推 staging 和 main,不删分支,不加 --no-verify。评论写给不懂代码的人看,不写任何用户个人资料。没有可处理的 issue 就什么都不做。 ``` + +## 注意 + +- 会话里的 `/loop` 7 天后过期,每周重开一次会话并重新粘贴(Claude 侧同理)。 +- 白名单不放行任意 `powershell -Command`:那样藏在里面的推送或读令牌会绕过所有规则。Gitea 只能经 `gitea.ps1`。