Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
634 changes: 8 additions & 626 deletions .github/workflows/issue-analysis.yml

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions .github/workflows/publish-model-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 \
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions packages/coding-agent/install-lock/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions packages/coding-agent/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
95 changes: 9 additions & 86 deletions scripts/issue-analysis.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
46 changes: 46 additions & 0 deletions scripts/model-publication-security.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
Loading