From 752cc4105d4bec16f7f068fea52a57f2192091f5 Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Sat, 17 Oct 2026 15:00:29 -0400 Subject: [PATCH 1/4] fix(coding-agent): harden publication security - Restrict model publication to trusted main sources and isolate upload credentials - Disable unsafe issue analysis and update vulnerable production dependencies - Require an explicit retained-artifact audit before changing source visibility --- .github/workflows/issue-analysis.yml | 634 +----------------- .github/workflows/publish-model-catalog.yml | 9 +- README.md | 4 +- package-lock.json | 20 +- .../install-lock/package-lock.json | 14 +- packages/coding-agent/npm-shrinkwrap.json | 14 +- packages/coding-agent/package.json | 2 +- scripts/issue-analysis.test.mjs | 95 +-- scripts/model-publication-security.test.mjs | 46 ++ 9 files changed, 96 insertions(+), 742 deletions(-) create mode 100644 scripts/model-publication-security.test.mjs diff --git a/.github/workflows/issue-analysis.yml b/.github/workflows/issue-analysis.yml index 7c81db931..a51443747 100644 --- a/.github/workflows/issue-analysis.yml +++ b/.github/workflows/issue-analysis.yml @@ -1,634 +1,16 @@ -# Runs the repo's /is prompt against an issue when the `diffex-analyze` label is added -# or when a repository maintainer comments `@issuron analyze` anywhere on an issue. -# -# Comment triggers can include one `#run-on-*` tag anywhere in the text: -# @issuron analyze #run-on-linux -> ubuntu-latest (default) -# @issuron analyze #run-on-windows -> windows-latest -# @issuron analyze #run-on-mac -> macos-latest -# -# Label triggers always run on the default Linux runner. Runner selection is -# intentionally restricted to hardcoded aliases in the authorization step. -# -# Setup required before this works: -# 1. Create a `diffex-analyze` GitHub environment on the repo and add a -# `DIFFEX_AUTH_JSON` secret containing the contents of a Diffex auth.json -# (~/.diffex/agent/auth.json). -# 2. Create the `diffex-analyze` label. -# 3. Authorization uses the repository token to require write or admin permission. -# 4. Add an environment secret `DIFFEX_GIST_TOKEN` on `diffex-analyze` with gist -# creation permission. The analysis job uses it to upload the exported -# session gist. -# 5. Add an environment secret `DIFFEX_AUTH_UPDATE_TOKEN` on `diffex-analyze` with -# permission to update this repo's environment secrets. The analysis job -# uses it to write back refreshed `DIFFEX_AUTH_JSON` contents. Fine-grained -# tokens need repository permission `Environments: read and write` on this -# repo; classic tokens need the `repo` scope. The `Secrets` permission is -# not enough: it only covers repository-level Actions secrets, while every -# `/repos/{owner}/{repo}/environments/{env}/secrets/*` endpoint is governed -# by `Environments`. Without it `gh secret set` fails with "Resource not -# accessible by personal access token" and the stored credential goes stale -# until its refresh token is rejected. -# -# `DIFFEX_AUTH_JSON` must come from a login dedicated to this workflow. OpenAI Codex -# rotates refresh tokens on every refresh, so sharing one auth.json with a -# developer machine invalidates whichever copy refreshes second, and analysis -# then fails with "OAuth refresh failed for openai-codex". -# -# The selected runner must have Node.js support plus gh, fd, and ripgrep. GitHub -# hosted runners are bootstrapped below; future self-hosted aliases should have -# those dependencies preinstalled or installable by the setup steps. -# -# The session runs in a high-entropy checkout directory so the recorded cwd is -# a unique string. Import the session into a local checkout with the -# /ir extension command (.diffex/extensions/import-repro.ts): -# diffex "/ir " - +# Disabled pending credential isolation and human review of generated output. +# Do not restore issue triggers, provider secrets, write tokens, or transcript uploads +# until untrusted issue content cannot access credentials or publish arbitrary output. name: Issue Analysis on: - issues: - types: [labeled] - issue_comment: - types: [created] - -permissions: - contents: read - issues: write + workflow_dispatch: -concurrency: - group: issue-analysis-${{ github.event.issue.number }} - cancel-in-progress: false +permissions: {} jobs: - authorize: + disabled: + if: ${{ false }} runs-on: ubuntu-latest - outputs: - should_run: ${{ steps.verify.outputs.should_run }} - extra_instructions: ${{ steps.verify.outputs.extra_instructions }} - runs_on: ${{ steps.verify.outputs.runs_on }} - runner_os: ${{ steps.verify.outputs.runner_os }} - runner_profile: ${{ steps.verify.outputs.runner_profile }} steps: - - name: Verify sender permission - id: verify - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const ANALYZE_LABEL = 'diffex-analyze'; - const TRIGGER_RE = /@issuron\s+analyze\b/i; - const RUN_ON_TAG_RE = /#run-on-([a-z0-9][a-z0-9_-]*)\b/gi; - const RUNNER_PROFILES = { - linux: { runsOn: 'ubuntu-latest', os: 'linux' }, - windows: { runsOn: 'windows-latest', os: 'windows' }, - mac: { runsOn: 'macos-latest', os: 'macos' }, - }; - const RUN_ON_ALIASES = { - linux: 'linux', - ubuntu: 'linux', - 'ubuntu-latest': 'linux', - windows: 'windows', - win: 'windows', - 'windows-latest': 'windows', - mac: 'mac', - macos: 'mac', - darwin: 'mac', - 'macos-latest': 'mac', - }; - - const username = context.payload.sender.login; - let extraInstructions = ''; - let runnerProfile = 'linux'; - - core.setOutput('should_run', 'false'); - core.setOutput('extra_instructions', ''); - core.setOutput('runs_on', JSON.stringify(RUNNER_PROFILES.linux.runsOn)); - core.setOutput('runner_os', RUNNER_PROFILES.linux.os); - core.setOutput('runner_profile', runnerProfile); - - if (context.eventName === 'issues') { - if (context.payload.action !== 'labeled' || context.payload.label?.name !== ANALYZE_LABEL) { - console.log('Not a diffex-analyze label event'); - return; - } - } else if (context.eventName === 'issue_comment') { - if (context.payload.issue.pull_request) { - console.log('Ignoring pull request comment'); - return; - } - const body = context.payload.comment.body || ''; - if (!TRIGGER_RE.test(body)) { - console.log('Comment does not contain an @issuron analyze trigger'); - return; - } - - const resolvedProfiles = new Set(); - const unknownTags = []; - for (const match of body.matchAll(RUN_ON_TAG_RE)) { - const tag = match[1].toLowerCase(); - const resolved = RUN_ON_ALIASES[tag]; - if (!resolved) { - unknownTags.push(tag); - } else { - resolvedProfiles.add(resolved); - } - } - - if (unknownTags.length > 0) { - core.setFailed(`Unknown issue analysis runner tag(s): ${unknownTags.map((tag) => `#run-on-${tag}`).join(', ')}`); - return; - } - if (resolvedProfiles.size > 1) { - core.setFailed( - `Conflicting issue analysis runner tags: ${Array.from(resolvedProfiles) - .map((profile) => `#run-on-${profile}`) - .join(', ')}`, - ); - return; - } - - runnerProfile = Array.from(resolvedProfiles)[0] || 'linux'; - extraInstructions = body.replace(TRIGGER_RE, ' ').replace(RUN_ON_TAG_RE, ' ').trim(); - } else { - console.log(`Unsupported event: ${context.eventName}`); - return; - } - - async function removeTriggerLabel() { - if (context.eventName !== 'issues') return; - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: ANALYZE_LABEL, - }); - } catch (error) { - if (error.status !== 404) throw error; - } - } - - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username, - }); - if (!['admin', 'write'].includes(data.permission)) { - await removeTriggerLabel(); - core.setFailed(`@${username} requires repository write or admin permission to trigger issue analysis.`); - return; - } - } catch { - await removeTriggerLabel(); - core.setFailed('Could not verify repository permission; refusing to run issue analysis.'); - return; - } - - if (context.eventName === 'issue_comment') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: [ANALYZE_LABEL], - }); - } - - const profile = RUNNER_PROFILES[runnerProfile]; - console.log(`Selected issue analysis runner profile: ${runnerProfile} (${JSON.stringify(profile.runsOn)})`); - core.setOutput('should_run', 'true'); - core.setOutput('extra_instructions', extraInstructions); - core.setOutput('runs_on', JSON.stringify(profile.runsOn)); - core.setOutput('runner_os', profile.os); - core.setOutput('runner_profile', runnerProfile); - - analyze: - needs: authorize - if: needs.authorize.outputs.should_run == 'true' - runs-on: ${{ fromJSON(needs.authorize.outputs.runs_on) }} - environment: diffex-analyze - timeout-minutes: 45 - concurrency: - group: issue-analysis-diffex-auth - cancel-in-progress: false - env: - ISSUE_ANALYSIS_MODEL: openai-codex/gpt-5.5 - ISSUE_ANALYSIS_THINKING: high - steps: - - name: Create high-entropy working directory name - id: workdir - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const crypto = require('crypto'); - core.setOutput('name', `diffex-ci-${crypto.randomBytes(16).toString('hex')}`); - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - path: ${{ steps.workdir.outputs.name }} - - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: npm - cache-dependency-path: ${{ steps.workdir.outputs.name }}/package-lock.json - - - name: Install system dependencies (Linux) - if: needs.authorize.outputs.runner_os == 'linux' - run: | - sudo apt-get update - sudo apt-get install -y fd-find ripgrep - sudo ln -sf "$(which fdfind)" /usr/local/bin/fd - - - name: Install system dependencies (macOS) - if: needs.authorize.outputs.runner_os == 'macos' - run: | - if ! command -v fd >/dev/null 2>&1; then - brew install fd - fi - if ! command -v rg >/dev/null 2>&1; then - brew install ripgrep - fi - - - name: Install system dependencies (Windows) - if: needs.authorize.outputs.runner_os == 'windows' - shell: pwsh - run: | - $packages = @() - if (-not (Get-Command fd -ErrorAction SilentlyContinue)) { - $packages += "fd" - } - if (-not (Get-Command rg -ErrorAction SilentlyContinue)) { - $packages += "ripgrep" - } - if ($packages.Count -gt 0) { - if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - throw "fd and ripgrep must be installed on Windows runners, or Chocolatey must be available to install them." - } - choco install $packages -y --no-progress - } - fd --version - rg --version - - - name: Install dependencies - working-directory: ${{ steps.workdir.outputs.name }} - run: npm ci --ignore-scripts - - - name: Build - working-directory: ${{ steps.workdir.outputs.name }} - run: npm run build - - - name: Write auth.json - id: write_auth - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - DIFFEX_AUTH_JSON: ${{ secrets.DIFFEX_AUTH_JSON }} - with: - script: | - const fs = require('fs'); - const path = require('path'); - - const authJson = process.env.DIFFEX_AUTH_JSON; - if (!authJson) { - throw new Error('DIFFEX_AUTH_JSON secret is not configured for the diffex-analyze environment'); - } - - const agentDir = path.join(process.env.RUNNER_TEMP, 'diffex-agent'); - fs.mkdirSync(agentDir, { recursive: true }); - const authPath = path.join(agentDir, 'auth.json'); - fs.writeFileSync(authPath, authJson, { mode: 0o600 }); - if (process.platform !== 'win32') { - fs.chmodSync(authPath, 0o600); - } - - - name: Run Diffex /is - id: run_diffex - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - DIFFEX_CODING_AGENT_DIR: ${{ runner.temp }}/diffex-agent - GH_TOKEN: ${{ github.token }} - ISSUE_URL: ${{ github.event.issue.html_url }} - EXTRA_INSTRUCTIONS: ${{ needs.authorize.outputs.extra_instructions }} - WORKDIR: ${{ steps.workdir.outputs.name }} - with: - script: | - const fs = require('fs'); - const path = require('path'); - const { spawn } = require('child_process'); - - const workdir = path.join(process.env.GITHUB_WORKSPACE, process.env.WORKDIR); - const outDir = path.join(process.env.RUNNER_TEMP, 'diffex-out'); - const sessionDir = path.join(outDir, 'session'); - fs.mkdirSync(sessionDir, { recursive: true }); - - let prompt = `/is ${process.env.ISSUE_URL}`; - if (process.env.EXTRA_INSTRUCTIONS) { - prompt += '\n\nAdditional instructions from @issuron analyze comment:\n'; - prompt += process.env.EXTRA_INSTRUCTIONS; - } - - const outputPath = path.join(outDir, 'output.md'); - const output = fs.createWriteStream(outputPath); - const args = [ - 'packages/coding-agent/src/cli.ts', - '-p', - '--approve', - '--session-dir', - sessionDir, - '--model', - process.env.ISSUE_ANALYSIS_MODEL, - '--thinking', - process.env.ISSUE_ANALYSIS_THINKING, - prompt, - ]; - - const exitCode = await new Promise((resolve, reject) => { - const child = spawn('node', args, { - cwd: workdir, - env: process.env, - stdio: ['ignore', 'pipe', 'pipe'], - }); - child.stdout.on('data', (chunk) => { - process.stdout.write(chunk); - output.write(chunk); - }); - child.stderr.on('data', (chunk) => { - process.stderr.write(chunk); - }); - child.on('error', reject); - child.on('close', resolve); - }); - await new Promise((resolve) => output.end(resolve)); - - if (exitCode !== 0) { - throw new Error(`Diffex /is failed with exit code ${exitCode}`); - } - - - name: Persist refreshed auth.json - if: always() && steps.write_auth.outcome == 'success' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.DIFFEX_AUTH_UPDATE_TOKEN }} - DIFFEX_AUTH_JSON: ${{ secrets.DIFFEX_AUTH_JSON }} - DIFFEX_CODING_AGENT_DIR: ${{ runner.temp }}/diffex-agent - with: - script: | - const fs = require('fs'); - const path = require('path'); - const { spawn } = require('child_process'); - - if (!process.env.GH_TOKEN) { - throw new Error('DIFFEX_AUTH_UPDATE_TOKEN is not configured for the diffex-analyze environment'); - } - - const authPath = path.join(process.env.DIFFEX_CODING_AGENT_DIR, 'auth.json'); - if (!fs.existsSync(authPath)) { - core.warning('auth.json was not created; skipping auth persistence'); - return; - } - - const authJson = fs.readFileSync(authPath, 'utf8'); - let parsed; - try { - parsed = JSON.parse(authJson); - } catch (error) { - throw new Error(`Refusing to persist malformed auth.json: ${error instanceof Error ? error.message : String(error)}`); - } - - const codexAuth = parsed['openai-codex']; - if (codexAuth?.type !== 'oauth' || typeof codexAuth.refresh !== 'string' || codexAuth.refresh.length === 0) { - throw new Error('Refusing to persist auth.json without openai-codex OAuth refresh credentials'); - } - - if (authJson === process.env.DIFFEX_AUTH_JSON) { - console.log('auth.json is unchanged; skipping secret update'); - return; - } - - await new Promise((resolve, reject) => { - const child = spawn( - 'gh', - ['secret', 'set', 'DIFFEX_AUTH_JSON', '--env', 'diffex-analyze', '--repo', process.env.GITHUB_REPOSITORY], - { env: process.env, stdio: ['pipe', 'inherit', 'pipe'] }, - ); - const stderrChunks = []; - child.stderr.on('data', (chunk) => { - stderrChunks.push(chunk); - process.stderr.write(chunk); - }); - child.on('error', reject); - child.on('close', (code) => { - if (code === 0) { - resolve(); - return; - } - const details = Buffer.concat(stderrChunks).toString().trim(); - reject( - new Error( - `gh secret set failed with exit code ${code}${details ? `: ${details}` : ''}. ` + - 'DIFFEX_AUTH_UPDATE_TOKEN needs repository permission "Environments: read and write" on this repo.', - ), - ); - }); - child.stdin.end(authJson); - }); - - - name: Export session files - id: export_session_files - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - DIFFEX_CODING_AGENT_DIR: ${{ runner.temp }}/diffex-agent - WORKDIR: ${{ steps.workdir.outputs.name }} - with: - script: | - const fs = require('fs'); - const path = require('path'); - const { spawn } = require('child_process'); - - function findFirstJsonl(dir) { - if (!fs.existsSync(dir)) return undefined; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const entryPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - const nested = findFirstJsonl(entryPath); - if (nested) return nested; - } else if (entry.isFile() && entry.name.endsWith('.jsonl')) { - return entryPath; - } - } - return undefined; - } - - const outDir = path.join(process.env.RUNNER_TEMP, 'diffex-out'); - const sessionFile = findFirstJsonl(path.join(outDir, 'session')); - if (!sessionFile) { - throw new Error('No session jsonl file found'); - } - - const sessionJsonl = path.join(outDir, 'session.jsonl'); - const sessionHtml = path.join(outDir, 'session.html'); - fs.copyFileSync(sessionFile, sessionJsonl); - - const workdir = path.join(process.env.GITHUB_WORKSPACE, process.env.WORKDIR); - const exitCode = await new Promise((resolve, reject) => { - const child = spawn( - 'node', - ['packages/coding-agent/src/cli.ts', '--no-extensions', '--export', sessionJsonl, sessionHtml], - { cwd: workdir, env: process.env, stdio: 'inherit' }, - ); - child.on('error', reject); - child.on('close', resolve); - }); - if (exitCode !== 0) { - throw new Error(`session export failed with exit code ${exitCode}`); - } - - - name: Upload session gist - id: gist - if: always() && steps.export_session_files.outcome == 'success' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - DIFFEX_GIST_TOKEN: ${{ secrets.DIFFEX_GIST_TOKEN }} - with: - github-token: ${{ secrets.DIFFEX_GIST_TOKEN }} - script: | - const fs = require('fs'); - const path = require('path'); - - if (!process.env.DIFFEX_GIST_TOKEN) { - throw new Error('DIFFEX_GIST_TOKEN is not configured'); - } - - const outDir = path.join(process.env.RUNNER_TEMP, 'diffex-out'); - const files = {}; - for (const filename of ['session.html', 'session.jsonl']) { - files[filename] = { content: fs.readFileSync(path.join(outDir, filename), 'utf8') }; - } - - const response = await github.rest.gists.create({ - public: false, - files, - }); - const gistUrl = response.data.html_url; - const gistId = response.data.id; - core.setOutput('url', gistUrl); - core.setOutput('id', gistId); - core.setOutput('share_url', gistUrl); - - - name: Comment with session import instructions - if: always() && steps.run_diffex.outcome == 'success' && steps.gist.outcome == 'success' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GIST_URL: ${{ steps.gist.outputs.url }} - GIST_ID: ${{ steps.gist.outputs.id }} - SHARE_URL: ${{ steps.gist.outputs.share_url }} - SESSION_JSONL: ${{ runner.temp }}/diffex-out/session.jsonl - with: - script: | - const fs = require('fs'); - - function extractLastAgentMessage(sessionPath) { - const lines = fs.readFileSync(sessionPath, 'utf8').split(/\r?\n/).filter(Boolean); - let lastText = ''; - - for (const line of lines) { - let entry; - try { - entry = JSON.parse(line); - } catch { - continue; - } - - if (entry.type !== 'message' || entry.message?.role !== 'assistant') continue; - - const content = entry.message.content; - const parts = []; - if (typeof content === 'string') { - parts.push(content); - } else if (Array.isArray(content)) { - for (const block of content) { - if (block?.type === 'text' && typeof block.text === 'string') { - parts.push(block.text); - } - } - } - - const text = parts.join('\n\n').trim(); - if (text) lastText = text; - } - - if (!lastText) return '_No assistant output found._'; - const maxLength = 55000; - if (lastText.length <= maxLength) return lastText; - return `${lastText.slice(0, maxLength)}\n\n_[truncated]_`; - } - - const gistId = process.env.GIST_ID; - const shareUrl = process.env.SHARE_URL || process.env.GIST_URL; - const lastAgentMessage = extractLastAgentMessage(process.env.SESSION_JSONL); - const body = [ - 'Diffex issue analysis finished.', - '', - `Session Gist: ${shareUrl}`, - '', - 'Continue locally from a checkout with:', - '', - '```sh', - `diffex "/ir ${gistId}"`, - '```', - '', - '
', - 'Agent analysis summary', - '', - lastAgentMessage, - '', - '
', - ].join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - - - name: Comment on analysis failure - if: always() && steps.run_diffex.outcome == 'failure' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GIST_URL: ${{ steps.gist.outputs.url }} - SHARE_URL: ${{ steps.gist.outputs.share_url }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - script: | - const shareUrl = process.env.SHARE_URL || process.env.GIST_URL; - const lines = ['Diffex issue analysis failed; no analysis was produced.', '', `Run: ${process.env.RUN_URL}`]; - if (shareUrl) { - lines.push(`Partial session Gist: ${shareUrl}`); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: lines.join('\n'), - }); - - - name: Remove trigger label - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'diffex-analyze', - }); - } catch (error) { - if (error.status !== 404) throw error; - } + - run: echo 'Issue analysis is disabled pending security redesign.' diff --git a/.github/workflows/publish-model-catalog.yml b/.github/workflows/publish-model-catalog.yml index 1d5e978ed..8a0c34143 100644 --- a/.github/workflows/publish-model-catalog.yml +++ b/.github/workflows/publish-model-catalog.yml @@ -36,7 +36,7 @@ permissions: jobs: generate: - if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} + if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} runs-on: ubuntu-latest env: SOURCE_REF: ${{ github.event.workflow_run.head_sha || inputs.source_ref || github.sha }} @@ -71,7 +71,7 @@ jobs: retention-days: 14 publish: - if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && inputs.publish) }} + if: ${{ github.ref == 'refs/heads/main' && ((github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.publish && inputs.source_ref == 'main')) }} needs: generate runs-on: ubuntu-latest environment: diffex-model-upload @@ -80,8 +80,6 @@ jobs: cancel-in-progress: true env: SOURCE_REF: ${{ github.event.workflow_run.head_sha || inputs.source_ref || github.sha }} - AWS_ACCESS_KEY_ID: ${{ secrets.DIFFEX_ARTIFACTS_R2_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.DIFFEX_ARTIFACTS_R2_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto AWS_EC2_METADATA_DISABLED: 'true' R2_ENDPOINT: https://67c0d357268b0fca6e0b465bb9d01b84.r2.cloudflarestorage.com @@ -138,6 +136,9 @@ jobs: - name: Publish model catalog to R2 if: steps.publication-window.outputs.allowed == 'true' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DIFFEX_ARTIFACTS_R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DIFFEX_ARTIFACTS_R2_SECRET_ACCESS_KEY }} run: | node scripts/publish-model-catalog.mjs \ --input .artifacts/model-catalog \ diff --git a/README.md b/README.md index defa2d4b2..8a35e9a97 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ Use `./diffex-test.sh` to run the Diffex-branded coding agent directly from this ## Distribution policy -The source repository remains private. +The source repository remains private pending an explicit publication review. +Changing visibility requires an audit of retained Actions logs, artifacts, source snapshots, debug maps, and linked session transcripts. +Artifact names such as `private-release-*` do not provide access protection. The approved release channel is the eleven public npm packages, including `@diffexai/diffex`. Standalone archives, Python, secondary package registries, and interactive CLI containers are deferred. Public packages include readable JavaScript and declarations, with Apache-2.0 metadata and complete applicable upstream notices. diff --git a/package-lock.json b/package-lock.json index 246380bb5..7a64ff3a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2735,9 +2735,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4768,9 +4768,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5137,9 +5137,9 @@ } }, "node_modules/undici": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", - "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -5624,7 +5624,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", - "undici": "8.9.0", + "undici": "8.12.2", "yaml": "2.9.0" }, "bin": { diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index e98550e74..01f3eec84 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -481,7 +481,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", - "undici": "8.9.0", + "undici": "8.12.2", "yaml": "2.9.0" }, "optionalDependencies": { @@ -1212,9 +1212,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1912,9 +1912,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", - "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index f5563800b..5385929c0 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -36,7 +36,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", - "undici": "8.9.0", + "undici": "8.12.2", "yaml": "2.9.0" }, "optionalDependencies": { @@ -1202,9 +1202,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1902,9 +1902,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", - "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 0725d1283..ce49744b2 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -71,7 +71,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", - "undici": "8.9.0", + "undici": "8.12.2", "yaml": "2.9.0" }, "overrides": { diff --git a/scripts/issue-analysis.test.mjs b/scripts/issue-analysis.test.mjs index f688881c5..4e55dd109 100644 --- a/scripts/issue-analysis.test.mjs +++ b/scripts/issue-analysis.test.mjs @@ -1,92 +1,15 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; -import vm from "node:vm"; +import { parse } from "yaml"; -const workflow = readFileSync( - new URL("../.github/workflows/issue-analysis.yml", import.meta.url), - "utf8", -); -const script = workflow - .split(" script: |\n")[1] - .split("\n analyze:")[0]; +const source = readFileSync(new URL("../.github/workflows/issue-analysis.yml", import.meta.url), "utf8"); +const workflow = parse(source); -async function authorize(permission, options = {}) { - const outputs = {}; - const failures = []; - const requests = []; - const context = { - eventName: options.comment ? "issue_comment" : "issues", - repo: { owner: "diffexai", repo: "diffex" }, - issue: { number: 123 }, - payload: { - sender: { login: "reviewer" }, - action: "labeled", - label: { name: "diffex-analyze" }, - issue: {}, - comment: { body: options.body ?? "@issuron analyze" }, - }, - }; - await vm.runInNewContext(`(async () => {${script}\n})()`, { - context, - process: { env: {} }, - console: { log() {} }, - core: { - setOutput: (key, value) => { - outputs[key] = value; - }, - setFailed: (message) => failures.push(message), - }, - fetch: () => { - throw new Error("Unexpected external authorization"); - }, - github: { - rest: { - repos: { - getCollaboratorPermissionLevel: async (input) => { - requests.push(input); - if (options.failure) throw new Error("Controlled GitHub failure"); - return { data: { permission } }; - }, - }, - issues: { removeLabel: async () => {}, addLabels: async () => {} }, - }, - }, - }); - return { outputs, failures, requests }; -} - -for (const permission of ["admin", "write"]) { - test(`repository ${permission} can authorize without organization credentials`, async () => { - const result = await authorize(permission); - assert.equal(result.outputs.should_run, "true", result.failures.join("\n")); - assert.equal(result.requests[0].owner, "diffexai"); - assert.equal(result.requests[0].repo, "diffex"); - }); -} -for (const permission of ["read", "triage", "none", undefined]) { - test(`rejects ${permission} permission`, async () => { - const result = await authorize(permission); - assert.equal(result.outputs.should_run, "false"); - assert.equal(result.failures.length, 1); - }); -} -test("authorization service failures fail closed", async () => { - const result = await authorize("admin", { failure: true }); - assert.equal(result.outputs.should_run, "false"); - assert.equal(result.failures.length, 1); -}); -test("comment runner selection remains constrained", async () => { - const result = await authorize("write", { - comment: true, - body: "@issuron analyze #run-on-mac", - }); - assert.equal(result.outputs.should_run, "true"); - assert.equal(result.outputs.runner_os, "macos"); - const invalid = await authorize("write", { - comment: true, - body: "@issuron analyze #run-on-private", - }); - assert.equal(invalid.outputs.should_run, "false"); - assert.equal(invalid.requests.length, 0); +test("issue analysis cannot run tools or expose credentials", () => { + assert.deepEqual(Object.keys(workflow.on), ["workflow_dispatch"]); + assert.deepEqual(workflow.permissions, {}); + assert.deepEqual(Object.keys(workflow.jobs), ["disabled"]); + assert.equal(workflow.jobs.disabled.if, "${{ false }}"); + assert.doesNotMatch(source, /secrets\.|github\.token|--approve|gists\.create|upload-artifact/); }); diff --git a/scripts/model-publication-security.test.mjs b/scripts/model-publication-security.test.mjs new file mode 100644 index 000000000..6dedfdeda --- /dev/null +++ b/scripts/model-publication-security.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import vm from "node:vm"; +import { parse } from "yaml"; + +const workflow = parse(readFileSync(new URL("../.github/workflows/publish-model-catalog.yml", import.meta.url), "utf8")); + +function allowed(job, overrides = {}, inputs = {}) { + const github = { + repository: "diffexai/diffex", + ref: "refs/heads/main", + event_name: "workflow_run", + event: { workflow_run: { + conclusion: "success", event: "push", head_branch: "main", + head_repository: { full_name: "diffexai/diffex" }, + } }, + ...overrides, + }; + return Boolean(vm.runInNewContext(workflow.jobs[job].if.replace(/^\$\{\{\s*|\s*\}\}$/g, ""), { github, inputs })); +} + +for (const job of ["generate", "publish"]) { + test(`${job} rejects untrusted workflow completions`, () => { + assert.equal(allowed(job), true); + for (const change of [ + { head_repository: { full_name: "attacker/diffex" } }, + { event: "pull_request" }, { head_branch: "feature" }, { conclusion: "failure" }, + ]) { + assert.equal(allowed(job, { event: { workflow_run: { + conclusion: "success", event: "push", head_branch: "main", + head_repository: { full_name: "diffexai/diffex" }, ...change, + } } }), false, JSON.stringify(change)); + } + }); +} + +test("publication accepts only main for scheduled and manual runs", () => { + assert.equal(allowed("publish", { event_name: "schedule" }), true); + assert.equal(allowed("publish", { event_name: "pull_request" }), false); + assert.equal(allowed("publish", { event_name: "workflow_dispatch" }, { publish: true, source_ref: "main" }), true); + for (const inputs of [{ publish: false, source_ref: "main" }, { publish: true, source_ref: "feature" }]) { + assert.equal(allowed("publish", { event_name: "workflow_dispatch" }, inputs), false); + } + assert.equal(allowed("publish", { event_name: "schedule", ref: "refs/heads/feature" }), false); +}); From 8e8ada3df4944f2d1e40da9deb246fa7defad177 Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Sat, 17 Oct 2026 15:02:13 -0400 Subject: [PATCH 2/4] docs: update README overview and installation --- README.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8a35e9a97..a6fd8b708 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,28 @@ # Diffex -Diffex is a TypeScript agent harness and interactive coding agent. +Diffex is a self-evolving coding agent. It provides a multi-provider LLM API, a general-purpose agent runtime, a terminal UI, remote session protocols, and an extensible coding-agent CLI. -Diffex treats the agent harness as an optimization target instead of a static configuration. -The project is evolving the harness around observable behavior, diagnostic signals, and evidence-backed improvements. +Diffex treats the agent harness as an optimization target. +Diffex evolves through observable behavior, diagnostic signals, and evidence-backed improvements. -This repository is the current Node.js and TypeScript implementation. The archived Rust implementation is available at [diffexai/diffex-legacy](https://github.com/diffexai/diffex-legacy). Read more at [diffex.ai](https://diffex.ai/) and in the [Diffex white paper](https://diffex.ai/white-paper). -## Fork Status +Diffex builds on the [Pi Agent Harness](https://github.com/earendil-works/pi) to extend its capabilities. +Use `./diffex-test.sh` to run Diffex directly from this repository. -Diffex is a fork of the [Pi Agent Harness](https://github.com/earendil-works/pi). -The workspace packages and CLI now use Diffex-owned names. -Use `./diffex-test.sh` to run the Diffex-branded coding agent directly from this repository. +## Installation + +Diffex is released on npm as [`@diffexai/diffex`](https://www.npmjs.com/package/@diffexai/diffex?activeTab=readme). +Install it globally and launch the CLI: + +```bash +npm install -g @diffexai/diffex +diffex +``` ## Workspace Packages @@ -90,14 +96,6 @@ npm run check `npm run check` runs formatting, linting, dependency policy checks, and TypeScript validation. `./test.sh` runs the non-E2E test suites without activating tests that require provider credentials. -## Plan History - -`update_plan` saves each changed plan in the session history and returns the complete resolved plan in its tool result, including step IDs, statuses, and success criteria. -Clearing the plan returns `Plan cleared.` -Successful compaction summaries include the latest plan snapshot, or an empty plan when it was cleared or absent, so resumed and forked sessions retain the task context. -When `/tree` summarizes an abandoned branch, its saved summary identifies that branch's plan references as historical and includes the selected branch's authoritative plan snapshot. -Ordinary requests reuse the existing conversation prefix without appending a plan message to every model request. - ## Documentation - [Coding-agent documentation](packages/coding-agent/docs/index.md) From 006e464e149f16fd14e36624bd9adf98d0c29e9d Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Sat, 17 Oct 2026 15:07:42 -0400 Subject: [PATCH 3/4] Revert "docs: update README overview and installation" This reverts commit 8e8ada3df4944f2d1e40da9deb246fa7defad177. --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a6fd8b708..8a35e9a97 100644 --- a/README.md +++ b/README.md @@ -21,28 +21,22 @@ # Diffex -Diffex is a self-evolving coding agent. +Diffex is a TypeScript agent harness and interactive coding agent. It provides a multi-provider LLM API, a general-purpose agent runtime, a terminal UI, remote session protocols, and an extensible coding-agent CLI. -Diffex treats the agent harness as an optimization target. -Diffex evolves through observable behavior, diagnostic signals, and evidence-backed improvements. +Diffex treats the agent harness as an optimization target instead of a static configuration. +The project is evolving the harness around observable behavior, diagnostic signals, and evidence-backed improvements. +This repository is the current Node.js and TypeScript implementation. The archived Rust implementation is available at [diffexai/diffex-legacy](https://github.com/diffexai/diffex-legacy). Read more at [diffex.ai](https://diffex.ai/) and in the [Diffex white paper](https://diffex.ai/white-paper). -Diffex builds on the [Pi Agent Harness](https://github.com/earendil-works/pi) to extend its capabilities. -Use `./diffex-test.sh` to run Diffex directly from this repository. +## Fork Status -## Installation - -Diffex is released on npm as [`@diffexai/diffex`](https://www.npmjs.com/package/@diffexai/diffex?activeTab=readme). -Install it globally and launch the CLI: - -```bash -npm install -g @diffexai/diffex -diffex -``` +Diffex is a fork of the [Pi Agent Harness](https://github.com/earendil-works/pi). +The workspace packages and CLI now use Diffex-owned names. +Use `./diffex-test.sh` to run the Diffex-branded coding agent directly from this repository. ## Workspace Packages @@ -96,6 +90,14 @@ npm run check `npm run check` runs formatting, linting, dependency policy checks, and TypeScript validation. `./test.sh` runs the non-E2E test suites without activating tests that require provider credentials. +## Plan History + +`update_plan` saves each changed plan in the session history and returns the complete resolved plan in its tool result, including step IDs, statuses, and success criteria. +Clearing the plan returns `Plan cleared.` +Successful compaction summaries include the latest plan snapshot, or an empty plan when it was cleared or absent, so resumed and forked sessions retain the task context. +When `/tree` summarizes an abandoned branch, its saved summary identifies that branch's plan references as historical and includes the selected branch's authoritative plan snapshot. +Ordinary requests reuse the existing conversation prefix without appending a plan message to every model request. + ## Documentation - [Coding-agent documentation](packages/coding-agent/docs/index.md) From 1f1b2a5a87cf8f45af6869951c17683b76c002af Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Sat, 17 Oct 2026 15:23:02 -0400 Subject: [PATCH 4/4] docs(coding-agent): prepare source publication - Record disclosure evidence and the controlled visibility transition. - Preserve vendored license terms and clarify the approved publication scope. --- README.md | 4 +- SECURITY.md | 6 +- docs/release/source-publication.md | 119 +++++++++++++++++++++++++++ legal/upstream/marked-18.0.5.LICENSE | 44 ++++++++++ legal/upstream/sources.json | 5 ++ 5 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 docs/release/source-publication.md create mode 100644 legal/upstream/marked-18.0.5.LICENSE diff --git a/README.md b/README.md index 8a35e9a97..b468f5c07 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,13 @@ Use `./diffex-test.sh` to run the Diffex-branded coding agent directly from this The source repository remains private pending an explicit publication review. Changing visibility requires an audit of retained Actions logs, artifacts, source snapshots, debug maps, and linked session transcripts. Artifact names such as `private-release-*` do not provide access protection. -The approved release channel is the eleven public npm packages, including `@diffexai/diffex`. +The approved release channel is the public npm package `@diffexai/diffex`, which bundles the required runtime workspaces. Standalone archives, Python, secondary package registries, and interactive CLI containers are deferred. Public packages include readable JavaScript and declarations, with Apache-2.0 metadata and complete applicable upstream notices. They exclude source maps, TypeScript examples, and native source build files. Delivered implementation code remains inspectable; private source hosting and compilation do not guarantee confidentiality. See the [distribution review](docs/release/distribution-review.md) for exact scope and release prerequisites. +See the [source publication review](docs/release/source-publication.md) for the separate repository visibility transition. ## Local Setup @@ -124,6 +125,7 @@ Participation in the project is governed by the [Code of Conduct](CODE_OF_CONDUC Do not commit credentials, secrets, local machine artifacts, build output, or private deployment state. Report sensitive issues privately through the repository maintainers or the contact paths on [diffex.ai](https://diffex.ai/). +See [SECURITY.md](SECURITY.md) for reporting guidance and the execution trust boundary. ## License diff --git a/SECURITY.md b/SECURITY.md index 821beab91..56191e32f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,9 +23,9 @@ coding agent trivially and this cannot be protected against. ## Reporting a Vulnerability -If you believe you found a security vulnerability in Diffex or another package in -this repository, contact a Diffex repository owner privately through your existing repository access. -A dedicated public security mailbox and GitHub private reporting channel have not yet been verified. +Report suspected vulnerabilities privately through the repository Security tab when private vulnerability reporting is available. +Until that channel is enabled, contact a Diffex repository owner privately through your existing repository access or the contact paths on [diffex.ai](https://diffex.ai/). +Do not put credentials, personal data, or exploit details in public issues or pull requests. Do not send sensitive reports to an upstream project. Please include: diff --git a/docs/release/source-publication.md b/docs/release/source-publication.md new file mode 100644 index 000000000..b163778ea --- /dev/null +++ b/docs/release/source-publication.md @@ -0,0 +1,119 @@ +# Source publication review + +This review prepares a controlled private-to-public transition for `diffexai/diffex`. +It does not change repository visibility or authorize package publication. +CI failures and Dependabot remediation are deferred at the owner's explicit request. +Neither is represented as resolved. +The reviewed source is ready for the controlled transition below, subject to landing the reviewed changes and pausing Actions before the visibility switch. +It is not ready for unrestricted public workflow execution until the public controls are verified. + +## Publication scope + +Making this repository public exposes reachable branches and tags, historical source, issues and pull requests, comments, and retained Actions evidence. +The approved npm payload is a smaller and separate distribution scope. +Source publication includes development records under `docs/release`, agent prompts, evaluation adapters, test fixtures, and native helper source. +Do not rely on `private` package flags or artifact names to keep those materials confidential. +Do not change visibility if any of this material must remain private. + +The current npm policy permits only `@diffexai/diffex`, with bundled runtime workspaces. +Opening the source does not authorize standalone binaries, container publication, or additional package registries. +Historical distribution reviews describe the policy in force when they were written; they are not current claims of access protection after a visibility change. + +## Review evidence + +The review started from source commit `006e464e149f16fd14e36624bd9adf98d0c29e9d` on 2026-10-17. +A fresh remote mirror contains 242 branch refs, nine tag refs, 246 pull-request refs, and 6,797 reachable commits. +Gitleaks 8.30.1 was downloaded from its official release and its archive SHA-256 matched the published checksum: `b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5`. +The scanner used its default rules, full redaction, and ignored inline allow comments. + +Gitleaks reported 6,401 commits scanned and 42 generic-key matches. +The tracked working-tree scan reported nine matches. +Current-tree matches are test idempotency strings and a keyboard binding. +Historical matches additionally include model identifiers, an HTTP beta-feature header, and upstream OAuth client constants. +The historical Google Cloud and Google Antigravity OAuth files are byte-identical to their public Pi upstream files at the same commits. +Those client constants are not evidence of a private Diffex account credential. +The historical compaction transcript fixture also has the same Git blob identity as public upstream: `5e744e3d4b69b43789776719dd900af043fd8a8d`. + +The GitHub discussion inventory contains 239 issues/pull requests, 26 issue comments, 48 pull-request review comments, and 83 review bodies retrieved across all 235 PRs. +No GitHub release records or gist links were present in that inventory. +Wiki, Pages, and Discussions are disabled. +Four linked attachments were downloaded with authenticated access and visually inspected: a CLI schema-error screenshot and three demonstration UI screenshots, without visible credentials or personal data. +The four tracked coding-agent documentation images were also visually inspected; they contain demonstration UI or artwork, without visible credentials. + +All 501 inventoried artifacts and 2,021 workflow log archives were downloaded and scanned with nested-archive traversal enabled to depth eight. +Two older runs, `32985640027` and `32985547564`, returned HTTP 404 for their logs and could not be reviewed. +The combined scan also included issue/PR text, review bodies, and Git commit metadata and scanned approximately 778.55 MB of content. +It produced 272 matches: 252 generic-key matches in archived test strings, keyboard bindings, and model-file manifests, plus 20 commit SHA strings misidentified as Sourcegraph tokens. +The flagged token-plan manifest entries were verified against the SHA-256 of the embedded model files. +An additional pass unpacked 1,005 Docker build-record members and scanned approximately 119.54 MB without matches. +No private Diffex account credential was confirmed in these results. +This conclusion does not mean every scanner match was a placeholder: the historical public upstream OAuth client constants are real client identifiers already shipped upstream. +The closing remote-history refresh produced the same 42 findings and no new refs or commits. +A subsequent inventory found one additional model-catalog workflow run, `39061334078`, which was skipped without executing jobs; the artifact count remained 501. + +These checks are a targeted disclosure review, not a proof that no secret, personal data, or application vulnerability exists. +Expired or deleted content and third-party systems outside the repository cannot be certified by this review. +New commits, comments, artifacts, and runs after the inventory require a delta review before the visibility change. + +## Source notices + +The root `LICENSE` preserves the Apache-2.0 terms for Diffex and the upstream MIT terms and copyright for Pi-derived portions. +The vendored HTML-exporter libraries retain their bundle headers. +Their complete upstream texts are tracked at `legal/upstream/marked-18.0.5.LICENSE` and `legal/upstream/highlight-11.9.0.LICENSE`. +`legal/upstream/sources.json` records the source revisions. +Do not substitute the project license for those third-party terms. +The review verifies the notices present in the repository; it cannot establish ownership of unpublished contributions or third-party brand permissions on the owner's behalf. + +## Workflow review + +The pull-request-target gate uses the GitHub API and does not check out or execute pull-request code. +The contributor-approval workflow checks out the default branch and verifies maintainer permissions before changing its allowlist. +Issue-label workflows use static scripts and API arguments rather than shell interpolation of issue bodies. +Pull-request build and acceptance jobs run on GitHub-hosted runners without publication secrets. +Repository token defaults are read-only and Actions cannot approve pull requests. +Issue analysis remains disabled in GitHub and inert in the merged security workflow. +Model publication requires a successful same-repository push to main, or a main-only scheduled/manual publication. +Release and benchmark credentials remain environment-scoped and require the deployment restrictions below before Actions is resumed publicly. + +## Controlled visibility transition + +1. Land the reviewed security and notice changes on the default branch. + Resolve the merge procedure explicitly if the deferred CI failure prevents a protected merge; do not silently bypass protection. +2. Pause repository Actions and ensure no credential-bearing runs remain active. + Keep issue analysis disabled independently of the repository-wide pause. +3. Record the final source SHA and complete the delta disclosure review. + Preserve any evidence required for release recovery before deleting retained artifacts or runs. + Clear the four inventoried Actions caches, including npm caches and CodeQL databases, so pre-publication cache contents are not exposed to public workflow consumers. +4. Change visibility only after owner approval of the disclosure scope. +5. Before resuming Actions, verify or recreate main protection, release-tag protection, deployment policies, and scanning controls. +6. Resume only the reviewed workflows after the controls below pass readback. + Do not resume standalone/container publication or issue analysis as part of this transition. + +The latest private-repository API readback returned a plan restriction for rulesets and reported secret scanning disabled. +Earlier successful settings writes are therefore not sufficient evidence that protections remain enforced. +Public visibility should unlock the relevant controls; every control must be read back after the change. + +## Required public controls + +| Control | Required configuration | +| --- | --- | +| Main | Require PR approval, dismiss stale approvals, resolve review threads, require CI, and prohibit force pushes/deletion; no routine bypass. | +| Release tags | Restrict creation of `v*` tags to release maintainers; prohibit updates/deletion independently of creation privileges. | +| Model environment | Allow only branch `main`, not a tag named `main`. | +| Release environments | Allow only reviewed `main` recovery runs and `v*` tags; require Pepps233 approval and disallow admin bypass. | +| Benchmark environment | Restrict credential-bearing runs to reviewed main; retain only evidence approved for public disclosure. | +| Fork workflows | Require approval for all outside collaborators; retain read-only default token permissions and prohibit Actions PR approval. | +| Security controls | Enable secret scanning, push protection, Code Security/CodeQL, and private vulnerability reporting; verify actual status. | + +Pepps233 is the only currently observed repository admin. +An admin-role exception for tag creation also grants future admins that capability; use a dedicated release team if that is not intended. +With Pepps233 as the sole deployment reviewer, preventing self-review would block deployments initiated by that same account. +Independent deployment review requires another authorized reviewer or a different initiator. + +## Deferred follow-up + +- Fix CI and triage dependency alerts separately, as requested by the owner. +- Adapt the release command and contributor-approval automation to PR-based changes under main protection. +- Keep issue analysis disabled until credentials, execution, and output publication have independently enforced boundaries. +- Review CodeQL and secret-scanning results after public activation before enabling publication jobs. +- Keep non-source distribution channels deferred until their separate redistribution review is complete. diff --git a/legal/upstream/marked-18.0.5.LICENSE b/legal/upstream/marked-18.0.5.LICENSE new file mode 100644 index 000000000..4bd2d4a08 --- /dev/null +++ b/legal/upstream/marked-18.0.5.LICENSE @@ -0,0 +1,44 @@ +# License information + +## Contribution License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## Marked + +Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Markdown + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. diff --git a/legal/upstream/sources.json b/legal/upstream/sources.json index 43b4a8f45..9483c72a0 100644 --- a/legal/upstream/sources.json +++ b/legal/upstream/sources.json @@ -1,4 +1,9 @@ { + "marked-18.0.5.LICENSE": [ + "markedjs/marked", + "4063c638cb621c09091d41b26f323ff074416bb9", + "LICENSE" + ], "highlight-11.9.0.LICENSE": [ "highlightjs/highlight.js", "f47103d4f1ac1592c56904574d1fbf5bf2475605",