From b55c3e1a7f03004b0dd904dcdf4d36c8afecdcf8 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:38:19 +0000 Subject: [PATCH 1/2] fix(docker-git): keep issue context out of project AGENTS --- AGENTS.md | 9 -- README.md | 1 - .../docker-git/tests/core/templates.test.ts | 15 ++- .../src/core/templates-entrypoint/codex.ts | 4 - .../lib/src/core/templates-entrypoint/git.ts | 108 +++++++++++++++++- .../src/core/templates-entrypoint/tasks.ts | 87 +------------- scripts/pre-commit-secret-guard.sh | 70 +++++++++++- 7 files changed, 183 insertions(+), 111 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 71f7e871..7c87c6cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -373,12 +373,3 @@ describe("Message invariants", () => { Каждый эффект — это контролируемое взаимодействие с реальным миром. ПРИНЦИП: Сначала формализуем, потом программируем. - - -Issue workspace: #61 -Issue URL: https://github.com/ProverCoderAI/docker-git/issues/61 -Workspace path: /home/dev/provercoderai/docker-git/issue-61 - -Работай только над этим issue, если пользователь не попросил другое. -Если нужен первоисточник требований, открой Issue URL. - diff --git a/README.md b/README.md index 56fb39a1..4f07e09b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ Force modes: Agent context for issue workspaces: - Global `${CODEX_HOME}/AGENTS.md` includes workspace path + issue/PR context. -- For `issue-*` workspaces, docker-git creates `${TARGET_DIR}/AGENTS.md` (if missing) with issue context and auto-adds it to `.git/info/exclude`. ## Projects Root Layout diff --git a/packages/docker-git/tests/core/templates.test.ts b/packages/docker-git/tests/core/templates.test.ts index 6702e56b..b17bb4ad 100644 --- a/packages/docker-git/tests/core/templates.test.ts +++ b/packages/docker-git/tests/core/templates.test.ts @@ -73,6 +73,11 @@ describe("planFiles", () => { "GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"" ) expect(entrypointSpec.contents).toContain("token=\"$GITHUB_TOKEN\"") + expect(entrypointSpec.contents).toContain("issue_managed_start=''") + expect(entrypointSpec.contents).toContain("check_issue_managed_block_range") + expect(entrypointSpec.contents).toContain( + "push contains commit updating managed issue block in AGENTS.md" + ) expect(entrypointSpec.contents).toContain("CACHE_ROOT=\"/home/dev/.docker-git/.cache/git-mirrors\"") expect(entrypointSpec.contents).toContain("PACKAGE_CACHE_ROOT=\"/home/dev/.docker-git/.cache/packages\"") expect(entrypointSpec.contents).toContain("npm_config_store_dir") @@ -149,13 +154,17 @@ describe("planFiles", () => { if (entrypointSpec && entrypointSpec._tag === "File") { expect(entrypointSpec.contents).toContain("Доступные workspace пути:") expect(entrypointSpec.contents).toContain("Контекст workspace:") - expect(entrypointSpec.contents).toContain("Issue AGENTS.md:") - expect(entrypointSpec.contents).toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"") - expect(entrypointSpec.contents).toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"") expect(entrypointSpec.contents).toContain("docker_git_workspace_context_line()") expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-issue-5}\"") expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"") expect(entrypointSpec.contents).toContain("Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)") + expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_HINT_LINE=") + expect(entrypointSpec.contents).not.toContain("Issue AGENTS.md: __TARGET_DIR__/AGENTS.md") + expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"") + expect(entrypointSpec.contents).not.toContain( + "ISSUE_MANAGED_START=\"\"" + ) + expect(entrypointSpec.contents).not.toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"") } })) diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index ea190009..6578c3fb 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -206,7 +206,6 @@ PROJECT_LINE="Рабочая папка проекта (git clone): __TARGET_DIR WORKSPACES_LINE="Доступные workspace пути: __TARGET_DIR__" WORKSPACE_INFO_LINE="Контекст workspace: repository" FOCUS_LINE="Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__" -ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: n/a" INTERNET_LINE="Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе." if [[ "$REPO_REF" == issue-* ]]; then ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')" @@ -222,7 +221,6 @@ if [[ "$REPO_REF" == issue-* ]]; then else WORKSPACE_INFO_LINE="Контекст workspace: issue #$ISSUE_ID" fi - ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: __TARGET_DIR__/AGENTS.md" elif [[ "$REPO_REF" == refs/pull/*/head ]]; then PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')" PR_URL="" @@ -249,7 +247,6 @@ $PROJECT_LINE $WORKSPACES_LINE $WORKSPACE_INFO_LINE $FOCUS_LINE -$ISSUE_AGENTS_HINT_LINE $INTERNET_LINE $MANAGED_END EOF @@ -270,7 +267,6 @@ $PROJECT_LINE $WORKSPACES_LINE $WORKSPACE_INFO_LINE $FOCUS_LINE -$ISSUE_AGENTS_HINT_LINE $INTERNET_LINE $MANAGED_END EOF diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index c9447998..60bd3d76 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -82,17 +82,111 @@ export const renderEntrypointGitConfig = (config: TemplateConfig): string => ].join("\n\n") export const renderEntrypointGitHooks = (): string => - String.raw`# 3) Install global git hooks to protect main/master + String.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context HOOKS_DIR="/opt/docker-git/hooks" PRE_PUSH_HOOK="$HOOKS_DIR/pre-push" mkdir -p "$HOOKS_DIR" -if [[ ! -f "$PRE_PUSH_HOOK" ]]; then - cat <<'EOF' > "$PRE_PUSH_HOOK" + +cat <<'EOF' > "$PRE_PUSH_HOOK" #!/usr/bin/env bash set -euo pipefail protected_branches=("refs/heads/main" "refs/heads/master") allow_delete="${"${"}DOCKER_GIT_ALLOW_DELETE:-}" +zero_sha="0000000000000000000000000000000000000000" +issue_managed_start='' +issue_managed_end='' + +extract_issue_block() { + local ref="$1" + + if ! git cat-file -e "$ref" 2>/dev/null; then + return 0 + fi + + local awk_status=0 + if ! git cat-file -p "$ref" | awk -v start="$issue_managed_start" -v end="$issue_managed_end" ' + BEGIN { in_block = 0; found = 0 } + $0 == start { in_block = 1; found = 1 } + in_block == 1 { print } + $0 == end && in_block == 1 { in_block = 0; exit } + END { + if (found == 0) exit 3 + if (in_block == 1) exit 2 + } + '; then + awk_status=$? + if [[ "$awk_status" -eq 3 ]]; then + return 0 + fi + return "$awk_status" + fi +} + +commit_changes_issue_block() { + local commit="$1" + local parent="" + local commit_block="" + local parent_block="" + + if ! git diff-tree --no-commit-id --name-only -r "$commit" -- AGENTS.md | grep -qx "AGENTS.md"; then + return 1 + fi + + if ! commit_block="$(extract_issue_block "$commit:AGENTS.md")"; then + return 2 + fi + + parent="$(git rev-list --parents -n 1 "$commit" | awk '{print $2}')" + if [[ -n "$parent" ]]; then + if ! parent_block="$(extract_issue_block "$parent:AGENTS.md")"; then + return 2 + fi + fi + + if [[ "$commit_block" != "$parent_block" ]]; then + return 0 + fi + return 1 +} + +check_issue_managed_block_range() { + local local_sha="$1" + local remote_sha="$2" + local commits="" + local commit="" + local guard_status=0 + + if [[ "$local_sha" == "$zero_sha" ]]; then + return 0 + fi + + if [[ "$remote_sha" == "$zero_sha" ]]; then + commits="$(git rev-list "$local_sha" --not --remotes 2>/dev/null || true)" + if [[ -z "$commits" ]]; then + commits="$local_sha" + fi + else + commits="$(git rev-list "$remote_sha..$local_sha" 2>/dev/null || true)" + fi + + for commit in $commits; do + commit_changes_issue_block "$commit" + guard_status=$? + if [[ "$guard_status" -eq 0 ]]; then + echo "docker-git: push contains commit updating managed issue block in AGENTS.md: $commit" + echo "docker-git: this block is runtime context and must stay outside repository history." + return 1 + fi + if [[ "$guard_status" -eq 2 ]]; then + echo "docker-git: failed to parse managed issue block in AGENTS.md for commit $commit" + echo "docker-git: push blocked to prevent committing runtime workspace metadata." + return 1 + fi + done + + return 0 +} while read -r local_ref local_sha remote_ref remote_sha; do if [[ -z "$remote_ref" ]]; then @@ -105,7 +199,10 @@ while read -r local_ref local_sha remote_ref remote_sha; do exit 1 fi done - if [[ "$local_sha" == "0000000000000000000000000000000000000000" && "$remote_ref" == refs/heads/* ]]; then + if ! check_issue_managed_block_range "$local_sha" "$remote_sha"; then + exit 1 + fi + if [[ "$local_sha" == "$zero_sha" && "$remote_ref" == refs/heads/* ]]; then if [[ "$allow_delete" != "1" ]]; then echo "docker-git: deleting remote branches is disabled (set DOCKER_GIT_ALLOW_DELETE=1 to override)." exit 1 @@ -113,7 +210,6 @@ while read -r local_ref local_sha remote_ref remote_sha; do fi done EOF - chmod 0755 "$PRE_PUSH_HOOK" -fi +chmod 0755 "$PRE_PUSH_HOOK" git config --system core.hooksPath "$HOOKS_DIR" || true git config --global core.hooksPath "$HOOKS_DIR" || true` diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index b791355a..fd99fc43 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -133,89 +133,6 @@ const renderCloneCacheFinalize = (config: TemplateConfig): string => fi fi` -const renderIssueWorkspaceAgentsResolve = (): string => - `ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')" -ISSUE_URL="" -if [[ "$REPO_URL" == https://github.com/* ]]; then - ISSUE_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')" - if [[ -n "$ISSUE_REPO" ]]; then - ISSUE_URL="https://github.com/$ISSUE_REPO/issues/$ISSUE_ID" - fi -fi -if [[ -z "$ISSUE_URL" ]]; then - ISSUE_URL="n/a" -fi` - -const renderIssueWorkspaceAgentsManagedBlock = (): string => - `ISSUE_AGENTS_PATH="$TARGET_DIR/AGENTS.md" -ISSUE_MANAGED_START="" -ISSUE_MANAGED_END="" -ISSUE_MANAGED_BLOCK="$(cat < - `if [[ ! -e "$ISSUE_AGENTS_PATH" ]]; then - printf "%s\n" "$ISSUE_MANAGED_BLOCK" > "$ISSUE_AGENTS_PATH" -else - TMP_ISSUE_AGENTS_PATH="$(mktemp)" - if grep -qF "$ISSUE_MANAGED_START" "$ISSUE_AGENTS_PATH" && grep -qF "$ISSUE_MANAGED_END" "$ISSUE_AGENTS_PATH"; then - awk -v start="$ISSUE_MANAGED_START" -v end="$ISSUE_MANAGED_END" -v repl="$ISSUE_MANAGED_BLOCK" ' - BEGIN { in_block = 0 } - $0 == start { print repl; in_block = 1; next } - $0 == end { in_block = 0; next } - in_block == 0 { print } - ' "$ISSUE_AGENTS_PATH" > "$TMP_ISSUE_AGENTS_PATH" - else - sed \ - -e '/^# docker-git issue workspace$/d' \ - -e '/^Issue workspace: #/d' \ - -e '/^Issue URL: /d' \ - -e '/^Workspace path: /d' \ - -e '/^Работай только над этим issue, если пользователь не попросил другое[.]$/d' \ - -e '/^Если нужен первоисточник требований, открой Issue URL[.]$/d' \ - "$ISSUE_AGENTS_PATH" > "$TMP_ISSUE_AGENTS_PATH" - if [[ -s "$TMP_ISSUE_AGENTS_PATH" ]]; then - printf "\n" >> "$TMP_ISSUE_AGENTS_PATH" - fi - printf "%s\n" "$ISSUE_MANAGED_BLOCK" >> "$TMP_ISSUE_AGENTS_PATH" - fi - mv "$TMP_ISSUE_AGENTS_PATH" "$ISSUE_AGENTS_PATH" -fi -if [[ -e "$ISSUE_AGENTS_PATH" ]]; then - chown 1000:1000 "$ISSUE_AGENTS_PATH" || true -fi` - -const renderIssueWorkspaceAgentsExclude = (): string => - `EXCLUDE_PATH="$TARGET_DIR/.git/info/exclude" -if [[ -f "$ISSUE_AGENTS_PATH" ]]; then - touch "$EXCLUDE_PATH" - if ! grep -qx "AGENTS.md" "$EXCLUDE_PATH"; then - printf "%s\n" "AGENTS.md" >> "$EXCLUDE_PATH" - fi -fi` - -const renderIssueWorkspaceAgents = (): string => - [ - `if [[ "$CLONE_OK" -eq 1 && "$REPO_REF" == issue-* && -d "$TARGET_DIR/.git" ]]; then`, - renderIssueWorkspaceAgentsResolve(), - "", - renderIssueWorkspaceAgentsManagedBlock(), - "", - renderIssueWorkspaceAgentsWrite(), - "", - renderIssueWorkspaceAgentsExclude(), - "fi" - ].join("\n") - const renderCloneBody = (config: TemplateConfig): string => [ renderCloneBodyStart(config), @@ -224,9 +141,7 @@ const renderCloneBody = (config: TemplateConfig): string => "", renderCloneRemotes(config), "", - renderCloneCacheFinalize(config), - "", - renderIssueWorkspaceAgents() + renderCloneCacheFinalize(config) ].join("\n") const renderCloneFinalize = (): string => diff --git a/scripts/pre-commit-secret-guard.sh b/scripts/pre-commit-secret-guard.sh index 4789af8f..7865480c 100755 --- a/scripts/pre-commit-secret-guard.sh +++ b/scripts/pre-commit-secret-guard.sh @@ -12,6 +12,11 @@ command -v perl >/dev/null || { echo "ERROR: perl is required" >&2; exit 1; } SECRET_PATTERN='(\b(?:github_pat_|gho_|ghp_|ghu_|ghs_|ghr_|gha_)[A-Za-z0-9_]{20,255}\b|\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_-]{20,}\b|\bsk-ant-[A-Za-z0-9_-]{20,}\b|-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----)' HAS_GITLEAKS=0 +ISSUE_MANAGED_START='' +ISSUE_MANAGED_END='' + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT if command -v gitleaks >/dev/null 2>&1; then HAS_GITLEAKS=1 @@ -22,6 +27,69 @@ is_knowledge_path() { [[ "$path" =~ (^|/)\.(knowledge|knowlenge)(/|$) ]] } +extract_issue_block() { + local blob_ref="$1" + local out="$2" + + if ! git cat-file -e "$blob_ref" 2>/dev/null; then + : > "$out" + return 0 + fi + + local status=0 + if ! git cat-file -p "$blob_ref" | awk -v start="$ISSUE_MANAGED_START" -v end="$ISSUE_MANAGED_END" ' + BEGIN { in_block = 0; found = 0 } + $0 == start { in_block = 1; found = 1 } + in_block == 1 { print } + $0 == end && in_block == 1 { in_block = 0; exit } + END { + if (found == 0) exit 3 + if (in_block == 1) exit 2 + } + ' > "$out"; then + status=$? + if [ "$status" -eq 3 ]; then + : > "$out" + return 0 + fi + return "$status" + fi +} + +guard_issue_managed_agents_block() { + local path="AGENTS.md" + local head_block_path="$TMP_DIR/agents-head.block" + local staged_block_path="$TMP_DIR/agents-staged.block" + + if ! git diff --cached --name-only -- "$path" | grep -qx "$path"; then + return 0 + fi + + if ! extract_issue_block "HEAD:$path" "$head_block_path"; then + echo "ERROR: failed to parse managed issue block in HEAD:$path." >&2 + echo "Commit blocked: resolve malformed markers first." >&2 + return 1 + fi + + if ! extract_issue_block ":$path" "$staged_block_path"; then + echo "ERROR: staged $path has malformed managed issue block." >&2 + echo "Expected markers:" >&2 + echo " - $ISSUE_MANAGED_START" >&2 + echo " - $ISSUE_MANAGED_END" >&2 + return 1 + fi + + if ! cmp -s "$head_block_path" "$staged_block_path"; then + echo "ERROR: staged change updates docker-git managed issue block in $path." >&2 + echo "This runtime context must not be committed into the repository." >&2 + echo "Fix: git restore --staged --worktree -- $path" >&2 + echo "Then re-apply manual edits outside the managed block if needed." >&2 + return 1 + fi +} + +guard_issue_managed_agents_block + scan_with_gitleaks_file() { local file_path="$1" if [ "$HAS_GITLEAKS" -ne 1 ]; then @@ -67,8 +135,6 @@ redacted_count=0 manual_fix_files=() has_staged_files=0 -TMP_DIR=$(mktemp -d) -trap 'rm -rf "$TMP_DIR"' EXIT index=0 while IFS= read -r -d '' path; do From 91decdd1c1be6b37ebe1883b51c75bdb58ee5c87 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:43:09 +0000 Subject: [PATCH 2/2] refactor(lib): extract git hooks template constant --- packages/lib/src/core/templates-entrypoint/git.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index 60bd3d76..90b849b3 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -81,8 +81,8 @@ export const renderEntrypointGitConfig = (config: TemplateConfig): string => renderEntrypointGitIdentity(config) ].join("\n\n") -export const renderEntrypointGitHooks = (): string => - String.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context +const entrypointGitHooksTemplate = String + .raw`# 3) Install global git hooks to protect main/master + managed AGENTS context HOOKS_DIR="/opt/docker-git/hooks" PRE_PUSH_HOOK="$HOOKS_DIR/pre-push" mkdir -p "$HOOKS_DIR" @@ -213,3 +213,5 @@ EOF chmod 0755 "$PRE_PUSH_HOOK" git config --system core.hooksPath "$HOOKS_DIR" || true git config --global core.hooksPath "$HOOKS_DIR" || true` + +export const renderEntrypointGitHooks = (): string => entrypointGitHooksTemplate