Skip to content

Commit 870ba0d

Browse files
committed
Add Cortex Code GitHub Action + invocation source tracking
- Add actions/cortex-code/: GitHub Action for PR reviews and issue assistance via @Cortex-code mentions (TypeScript, Agent SDK, MCP) - Security hardened: signal cleanup, expanded token sanitization, prompt injection defense, cost guardrails, rate limiting, review mode - Add inline PR review comments via GitHub Reviews API - Add unified diff context for better review quality - Add CORTEX_CODE_ENTRYPOINT env var to plugin subprocess for telemetry - Add workflow for @Cortex-code trigger on this repo
1 parent 43d9fe4 commit 870ba0d

32 files changed

Lines changed: 2172 additions & 1 deletion
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test Cortex Code Action
2+
on:
3+
issue_comment:
4+
types: [created]
5+
6+
permissions:
7+
contents: read
8+
pull-requests: write
9+
issues: write
10+
11+
jobs:
12+
cortex-code:
13+
if: contains(github.event.comment.body, '@cortex-code')
14+
runs-on: ubuntu-latest
15+
concurrency:
16+
group: cortex-code-${{ github.event.issue.number }}
17+
cancel-in-progress: true
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: ./actions/cortex-code
22+
with:
23+
snowflake_account: ${{ secrets.SNOWFLAKE_ACCOUNT }}
24+
snowflake_user: ${{ secrets.SNOWFLAKE_USER }}
25+
snowflake_private_key: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}
26+
max_turns: "5"
27+
timeout_minutes: "3"

actions/cortex-code/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
bun.lock

actions/cortex-code/action.yml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
name: "Cortex Code Action"
2+
description: "GitHub Action that integrates Snowflake Cortex Code as an AI assistant for PRs and issues. Triggered by @cortex-code mentions."
3+
author: "Snowflake Cortex Code"
4+
5+
branding:
6+
icon: "code"
7+
color: "blue"
8+
9+
inputs:
10+
trigger_phrase:
11+
description: "Phrase that triggers the action in comments"
12+
required: false
13+
default: "@cortex-code"
14+
snowflake_account:
15+
description: "Snowflake account identifier (e.g., myorg-myaccount)"
16+
required: true
17+
snowflake_user:
18+
description: "Snowflake username for authentication"
19+
required: true
20+
snowflake_private_key:
21+
description: "Snowflake key-pair private key (PEM format). Mutually exclusive with snowflake_api_key."
22+
required: false
23+
snowflake_api_key:
24+
description: "Snowflake API key or PAT. Mutually exclusive with snowflake_private_key."
25+
required: false
26+
github_token:
27+
description: "GitHub token for API operations (PR comments, status checks)"
28+
required: false
29+
default: ${{ github.token }}
30+
model:
31+
description: "Model identifier for Cortex Code (e.g., auto, claude-sonnet-4-6, openai-gpt-5.2)"
32+
required: false
33+
default: "auto"
34+
allowed_tools:
35+
description: "Comma-separated additional tools to allow (e.g., SQL,WebSearch)"
36+
required: false
37+
default: ""
38+
disallowed_tools:
39+
description: "Comma-separated tools to deny"
40+
required: false
41+
default: ""
42+
system_prompt:
43+
description: "Additional system prompt text to append to the default"
44+
required: false
45+
default: ""
46+
branch_prefix:
47+
description: "Prefix for branches created by Cortex Code"
48+
required: false
49+
default: "cortex-code/"
50+
bot_name:
51+
description: "Display name for the bot in git operations"
52+
required: false
53+
default: "cortex-code[bot]"
54+
bot_id:
55+
description: "GitHub user ID for git operations"
56+
required: false
57+
default: "41898282"
58+
max_turns:
59+
description: "Maximum number of agentic turns before stopping"
60+
required: false
61+
default: "25"
62+
timeout_minutes:
63+
description: "Maximum execution time in minutes before the action is terminated"
64+
required: false
65+
default: "10"
66+
base_branch:
67+
description: "Base branch for new branches (defaults to repo default branch)"
68+
required: false
69+
default: ""
70+
prompt:
71+
description: "Direct prompt for agent mode. When provided, skips trigger detection and uses this as the instruction (e.g., for automated PR reviews)."
72+
required: false
73+
default: ""
74+
review_mode:
75+
description: "When true, restricts tools to read-only (no Bash, Write, Edit). Ideal for automated PR reviews."
76+
required: false
77+
default: ""
78+
79+
outputs:
80+
branch_name:
81+
description: "The branch created or used by Cortex Code"
82+
value: ${{ steps.run.outputs.branch_name }}
83+
session_id:
84+
description: "Cortex Code session ID for resuming"
85+
value: ${{ steps.run.outputs.session_id }}
86+
conclusion:
87+
description: "Result status: success or failure"
88+
value: ${{ steps.run.outputs.conclusion }}
89+
90+
runs:
91+
using: "composite"
92+
steps:
93+
- name: Install Bun
94+
uses: oven-sh/setup-bun@v2
95+
with:
96+
bun-version: "1.3.6"
97+
98+
- name: Install dependencies
99+
shell: bash
100+
working-directory: ${{ github.action_path }}
101+
run: bun install --production --frozen-lockfile || bun install --production
102+
103+
- name: Install Cortex Code CLI
104+
shell: bash
105+
run: |
106+
curl -LsS https://ai.snowflake.com/static/cc-scripts/install.sh | sh
107+
echo "$HOME/.local/bin" >> $GITHUB_PATH
108+
109+
- name: Run Cortex Code Action
110+
id: run
111+
shell: bash
112+
working-directory: ${{ github.action_path }}
113+
env:
114+
GITHUB_TOKEN: ${{ inputs.github_token }}
115+
SNOWFLAKE_ACCOUNT: ${{ inputs.snowflake_account }}
116+
SNOWFLAKE_USER: ${{ inputs.snowflake_user }}
117+
SNOWFLAKE_PRIVATE_KEY: ${{ inputs.snowflake_private_key }}
118+
SNOWFLAKE_API_KEY: ${{ inputs.snowflake_api_key }}
119+
INPUT_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
120+
INPUT_MODEL: ${{ inputs.model }}
121+
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
122+
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
123+
INPUT_SYSTEM_PROMPT: ${{ inputs.system_prompt }}
124+
INPUT_BRANCH_PREFIX: ${{ inputs.branch_prefix }}
125+
INPUT_BOT_NAME: ${{ inputs.bot_name }}
126+
INPUT_BOT_ID: ${{ inputs.bot_id }}
127+
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
128+
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
129+
INPUT_BASE_BRANCH: ${{ inputs.base_branch }}
130+
INPUT_PROMPT: ${{ inputs.prompt }}
131+
INPUT_REVIEW_MODE: ${{ inputs.review_mode }}
132+
run: bun run src/entrypoints/run.ts
1.56 KB
Loading

actions/cortex-code/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@snowflake-labs/cortex-code-action",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"repository": "https://github.com/Snowflake-Labs/snowflake-ai-kit",
7+
"scripts": {
8+
"format": "prettier --write .",
9+
"format:check": "prettier --check .",
10+
"test": "bun test",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@actions/core": "^1.10.1",
15+
"@actions/github": "^6.0.1",
16+
"cortex-code-agent-sdk": "latest",
17+
"@modelcontextprotocol/sdk": "^1.11.0",
18+
"@octokit/rest": "^21.1.1",
19+
"@octokit/graphql": "^8.2.2",
20+
"zod": "^3.24.4"
21+
},
22+
"devDependencies": {
23+
"@types/bun": "latest",
24+
"@types/node": "^20.0.0",
25+
"prettier": "^3.5.3",
26+
"typescript": "^5.8.3"
27+
}
28+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import * as os from "os";
4+
5+
export interface ConnectionConfig {
6+
account: string;
7+
user: string;
8+
privateKey?: string;
9+
apiKey?: string;
10+
}
11+
12+
let cleanupRegistered = false;
13+
14+
function registerCleanupHandlers(): void {
15+
if (cleanupRegistered) return;
16+
cleanupRegistered = true;
17+
18+
const emergencyCleanup = () => {
19+
cleanupConnection();
20+
process.exit(1);
21+
};
22+
23+
process.on("SIGTERM", emergencyCleanup);
24+
process.on("SIGINT", emergencyCleanup);
25+
}
26+
27+
export function setupSnowflakeConnection(config: ConnectionConfig): string {
28+
const connectionName = "cortex-code-action";
29+
const connectionsDir = path.join(os.homedir(), ".snowflake");
30+
const connectionsFile = path.join(connectionsDir, "connections.toml");
31+
32+
fs.mkdirSync(connectionsDir, { recursive: true });
33+
34+
let tomlContent: string;
35+
36+
if (config.privateKey) {
37+
const keyPath = path.join(connectionsDir, "action_key.p8");
38+
fs.writeFileSync(keyPath, config.privateKey, { mode: 0o600 });
39+
40+
tomlContent = `[${connectionName}]
41+
account = "${config.account}"
42+
user = "${config.user}"
43+
authenticator = "SNOWFLAKE_JWT"
44+
private_key_path = "${keyPath}"
45+
`;
46+
} else if (config.apiKey) {
47+
tomlContent = `[${connectionName}]
48+
account = "${config.account}"
49+
user = "${config.user}"
50+
token = "${config.apiKey}"
51+
authenticator = "oauth"
52+
`;
53+
} else {
54+
throw new Error(
55+
"Either snowflake_private_key or snowflake_api_key must be provided.",
56+
);
57+
}
58+
59+
tomlContent += `\n[default]\ndefault_connection_name = "${connectionName}"\n`;
60+
61+
fs.writeFileSync(connectionsFile, tomlContent, { mode: 0o600 });
62+
63+
registerCleanupHandlers();
64+
65+
return connectionName;
66+
}
67+
68+
export function cleanupConnection(): void {
69+
const connectionsDir = path.join(os.homedir(), ".snowflake");
70+
const connectionsFile = path.join(connectionsDir, "connections.toml");
71+
const keyPath = path.join(connectionsDir, "action_key.p8");
72+
73+
try {
74+
if (fs.existsSync(keyPath)) fs.unlinkSync(keyPath);
75+
if (fs.existsSync(connectionsFile)) fs.unlinkSync(connectionsFile);
76+
} catch {
77+
// Best effort cleanup
78+
}
79+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { setupSnowflakeConnection, cleanupConnection } from "./connection";
2+
export type { ConnectionConfig } from "./connection";
3+
export { installCortexCLI } from "./install";
4+
export { runCortexCode } from "./run";
5+
export type { RunCortexOptions, RunResult } from "./run";
6+
export type { McpServerConfig } from "./types";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as core from "@actions/core";
2+
3+
export async function installCortexCLI(): Promise<string> {
4+
// The CLI is installed in the composite action step via the install script.
5+
// This function verifies it's accessible and returns the path.
6+
const cliPath = process.env.CORTEX_CODE_CLI_PATH ?? "cortex";
7+
8+
try {
9+
const proc = Bun.spawn([cliPath, "--version"], {
10+
stdout: "pipe",
11+
stderr: "pipe",
12+
});
13+
const exitCode = await proc.exited;
14+
if (exitCode !== 0) {
15+
throw new Error(`cortex --version exited with code ${exitCode}`);
16+
}
17+
const version = await new Response(proc.stdout).text();
18+
core.info(`Cortex Code CLI version: ${version.trim()}`);
19+
return cliPath;
20+
} catch (error) {
21+
throw new Error(
22+
`Cortex Code CLI not found or not functional. ` +
23+
`Ensure it's installed: curl -LsS https://ai.snowflake.com/static/cc-scripts/install.sh | sh\n` +
24+
`Error: ${error}`,
25+
);
26+
}
27+
}

0 commit comments

Comments
 (0)