Skip to content

Commit 6e5076c

Browse files
feat: add Go framework support
Adds Go as a supported framework following the backend-framework pattern (Rails): - Integration.go enum entry at the end of the frameworks block, before the language fallbacks - GO_AGENT_CONFIG in src/frameworks/go/ — claims a go.mod with a module directive, gathers the go directive version as agent context, installs via go get github.com/posthog/posthog-go - goModulesPackageManager helper + FRAMEWORK_REGISTRY entry - bash-fence grammar for the go binary: get/build/vet/fmt/version/list plus go mod tidy/download/verify/graph/why; run/test/generate stay denied (arbitrary code execution), go mod edit stays denied (rewrites module requirements) - Pins framework: go on integration-go in the variant-resolution contract test, matching context-mill PR #267 which must release before this merges - Marks go.mod as a real framework target in the agentic manifest comment Generated-By: PostHog Code Task-Id: 9c949855-7611-48f2-b8b5-7aa4112543e5
1 parent ffd6225 commit 6e5076c

10 files changed

Lines changed: 216 additions & 5 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* Go wizard using posthog-agent with PostHog MCP */
2+
import * as fs from 'node:fs';
3+
import * as path from 'node:path';
4+
import type { WizardRunOptions } from '@utils/types';
5+
import type { FrameworkConfig } from '@lib/framework-config';
6+
import { goModulesPackageManager } from '@lib/detection/package-manager';
7+
import { Integration } from '@lib/constants';
8+
9+
type GoContext = {
10+
goVersion?: string;
11+
};
12+
13+
function readGoMod(installDir: string): string | undefined {
14+
const goModPath = path.join(installDir, 'go.mod');
15+
if (!fs.existsSync(goModPath)) {
16+
return undefined;
17+
}
18+
return fs.readFileSync(goModPath, 'utf-8');
19+
}
20+
21+
/** The `go 1.x` directive from go.mod (the toolchain floor, not a patch version). */
22+
function getGoVersion(installDir: string): string | undefined {
23+
const goMod = readGoMod(installDir);
24+
return goMod?.match(/^go\s+([\d.]+)/m)?.[1];
25+
}
26+
27+
export const GO_AGENT_CONFIG: FrameworkConfig<GoContext> = {
28+
metadata: {
29+
name: 'Go',
30+
integration: Integration.go,
31+
docsUrl: 'https://posthog.com/docs/libraries/go',
32+
gatherContext: (options: WizardRunOptions) => {
33+
const goVersion = getGoVersion(options.installDir);
34+
return Promise.resolve({ goVersion });
35+
},
36+
},
37+
38+
detection: {
39+
packageName: 'posthog-go',
40+
packageDisplayName: 'Go',
41+
usesPackageJson: false,
42+
getVersion: () => undefined,
43+
// A go.mod with a module directive marks a Go module root; a bare
44+
// directory containing .go files without one is not integratable.
45+
detect: (options) => {
46+
const goMod = readGoMod(options.installDir);
47+
return Promise.resolve(!!goMod && /^module\s+\S+/m.test(goMod));
48+
},
49+
detectPackageManager: goModulesPackageManager,
50+
},
51+
52+
environment: {
53+
uploadToHosting: false,
54+
getEnvVars: (apiKey: string, host: string) => ({
55+
POSTHOG_API_KEY: apiKey,
56+
POSTHOG_HOST: host,
57+
}),
58+
},
59+
60+
analytics: {
61+
getTags: (context) => ({
62+
goVersion: context.goVersion || 'unknown',
63+
}),
64+
},
65+
66+
prompts: {
67+
projectTypeDetection:
68+
'This is a Go project. Look for go.mod, go.sum, cmd/, internal/, and main packages to confirm.',
69+
packageInstallation:
70+
'Install the PostHog Go SDK with `go get github.com/posthog/posthog-go`. Do not manually edit go.mod or go.sum; the go tool updates them automatically. Run `go mod tidy` afterwards if imports change.',
71+
getAdditionalContextLines: (context) => {
72+
const lines = [
73+
`Framework docs ID: go (use posthog://docs/frameworks/go for documentation)`,
74+
];
75+
if (context.goVersion) {
76+
lines.push(`Go version (go.mod directive): ${context.goVersion}`);
77+
}
78+
lines.push(
79+
'Create one PostHog client per process and close it during graceful shutdown (`defer client.Close()`) so queued events flush.',
80+
);
81+
return lines;
82+
},
83+
},
84+
85+
ui: {
86+
successMessage: 'PostHog integration complete',
87+
estimatedDurationMinutes: 5,
88+
getOutroChanges: () => [
89+
'Analyzed your Go project structure',
90+
'Installed the posthog-go SDK via go get',
91+
'Initialized a shared PostHog client configured from environment variables',
92+
'Instrumented meaningful server events with client.Enqueue(posthog.Capture{...})',
93+
],
94+
getOutroNextSteps: () => [
95+
'Run your Go service and trigger the instrumented code paths',
96+
'Visit your PostHog dashboard to see incoming events',
97+
'Use client.Enqueue(posthog.Capture{...}) to track custom events',
98+
'Keep client.Close() in your graceful shutdown path so queued events flush',
99+
],
100+
},
101+
};

src/lib/__tests__/wizard-can-use-tool.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ describe('bash fence — allows real toolchain commands (from skills + field log
139139
expect(allow('carthage bootstrap')).toBe('allow');
140140
});
141141

142+
it('go ecosystem', () => {
143+
expect(allow('go get github.com/posthog/posthog-go')).toBe('allow');
144+
expect(allow('go mod tidy')).toBe('allow');
145+
expect(allow('go mod download')).toBe('allow');
146+
expect(allow('go build ./...')).toBe('allow');
147+
expect(allow('go vet ./...')).toBe('allow');
148+
expect(allow('go fmt ./...')).toBe('allow');
149+
expect(allow('go list -m all')).toBe('allow');
150+
// run/test/generate execute project code; mod edit rewrites requirements.
151+
expect(allow('go run main.go')).toBe('deny');
152+
expect(allow('go test ./...')).toBe('deny');
153+
expect(allow('go generate ./...')).toBe('deny');
154+
expect(allow('go mod edit -replace example.com/x=evil.example/x')).toBe(
155+
'deny',
156+
);
157+
expect(allow('go tool pprof')).toBe('deny');
158+
});
159+
142160
it('android/jvm ecosystem', () => {
143161
expect(allow('./gradlew assembleDebug')).toBe('allow');
144162
expect(allow('./gradlew :app:assembleDebug')).toBe('allow');
@@ -194,7 +212,7 @@ describe('bash fence — attack corpus (one test per bypass vector)', () => {
194212
expect(allow('bundle exec rspec')).toBe('deny');
195213
expect(allow('composer run-script evil')).toBe('deny');
196214
expect(allow('cargo run')).toBe('deny'); // no rust framework -> whole binary denied
197-
expect(allow('go get github.com/x/y')).toBe('deny'); // no go framework
215+
expect(allow('go run main.go')).toBe('deny'); // arbitrary code execution
198216
});
199217

200218
it('shell injection: separators, subshells, chaining', () => {

src/lib/agent/bash-fence.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ const SIMPLE_MANAGERS: Record<string, readonly string[]> = {
8686
carthage: ['bootstrap', 'update'],
8787
};
8888

89+
// go's verb list is closed: dependency + verify commands only. run/test/generate
90+
// execute project-defined code; `go mod edit` can rewrite module requirements
91+
// to arbitrary sources, so only the read/refresh mod subcommands are allowed.
92+
const GO_SUBCOMMANDS = ['get', 'build', 'vet', 'fmt', 'version', 'list'];
93+
const GO_MOD_SUBCOMMANDS = ['tidy', 'download', 'verify', 'graph', 'why'];
94+
8995
// Gradle tasks are verb-anchored camelCase: assembleDebug yes, publishToMavenCentral no.
9096
const GRADLE_EXACT_TASKS = new Set(['build', 'clean', 'dependencies']);
9197
const GRADLE_TASK_VERBS = ['assemble', 'compile', 'bundle', 'lint'];
@@ -102,7 +108,8 @@ const ALLOWED_TOOLS_SUMMARY =
102108
'composer (install|require|update|remove|show), bundle (install|add|remove|update|show|exec <lint tool>), ' +
103109
'gem (install|uninstall|list|search), swift (package|build), pod (install|update|search), carthage (bootstrap|update), ' +
104110
'xcodebuild (build/clean/archive actions), gradle/gradlew (build|clean|dependencies|assemble*/compile*/bundle*/lint* tasks), ' +
105-
'mvn (install|compile|package|verify|dependency:tree).';
111+
'mvn (install|compile|package|verify|dependency:tree), ' +
112+
'go (get|build|vet|fmt|version|list, mod tidy/download/verify/graph/why).';
106113

107114
function deny(analyticsReason: string, message: string): BashFenceDecision {
108115
return { allowed: false, message, analyticsReason };
@@ -273,6 +280,25 @@ function commandDecision(command: string): BashFenceDecision {
273280
) {
274281
return { allowed: true };
275282
}
283+
if (bin === 'go') {
284+
if (parts[1] === 'mod') {
285+
if (parts[2] && GO_MOD_SUBCOMMANDS.includes(parts[2]))
286+
return { allowed: true };
287+
return denyCommand(
288+
command,
289+
`Allowed go mod subcommands: ${GO_MOD_SUBCOMMANDS.join(', ')}.`,
290+
);
291+
}
292+
if (parts[1] && GO_SUBCOMMANDS.includes(parts[1])) return { allowed: true };
293+
return denyCommand(
294+
command,
295+
`Allowed go subcommands: ${GO_SUBCOMMANDS.join(
296+
', ',
297+
)}, mod <${GO_MOD_SUBCOMMANDS.join(
298+
'|',
299+
)}>. go run/test/generate execute project code and are not allowed.`,
300+
);
301+
}
276302
if (bin === 'uv' && parts[1] === 'pip') {
277303
if (parts[2] && PIP_SUBCOMMANDS.includes(parts[2]))
278304
return { allowed: true };

src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const INTEGRATION_ENTRIES = [
4949
{ id: 'integration-javascript_web', framework: 'javascript_web' },
5050
{ id: 'integration-ruby', framework: 'ruby' },
5151
{ id: 'integration-elixir' },
52-
{ id: 'integration-go' },
52+
{ id: 'integration-go', framework: 'go' },
5353
{ id: 'integration-swift', framework: 'swift' },
5454
{ id: 'integration-kmp', framework: 'kmp' },
5555
{ id: 'integration-flutter' },

src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export enum Integration {
100100
swift = 'swift',
101101
android = 'android',
102102
rails = 'rails',
103+
go = 'go',
103104

104105
// Language fallbacks. Keep javascriptNode last: it matches any package.json.
105106
python = 'python',

src/lib/detection/__tests__/framework.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { detectFramework } from '@lib/detection/framework';
55
import { Integration } from '@lib/constants';
66
import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent';
77
import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent';
8+
import { GO_AGENT_CONFIG } from '../../../frameworks/go/go-wizard-agent';
89

910
/** A throwaway project dir seeded with the given files. */
1011
function makeProject(files: Record<string, string>): string {
@@ -137,6 +138,27 @@ describe('detectFramework (end-to-end over real project dirs)', () => {
137138
);
138139
});
139140

141+
test('a Go module project resolves to go', async () => {
142+
const opts = project({
143+
'go.mod': 'module example.com/app\n\ngo 1.22\n',
144+
'main.go': 'package main',
145+
});
146+
await expect(detectFramework(opts.installDir)).resolves.toBe(
147+
Integration.go,
148+
);
149+
});
150+
151+
test('a Go project with an embedded frontend package.json still resolves to go', async () => {
152+
const opts = project({
153+
'go.mod': 'module example.com/app\n\ngo 1.22\n',
154+
'package.json': JSON.stringify({ devDependencies: { esbuild: '^0.20' } }),
155+
'package-lock.json': '{}',
156+
});
157+
await expect(detectFramework(opts.installDir)).resolves.toBe(
158+
Integration.go,
159+
);
160+
});
161+
140162
test('a Flutter project is not claimed by anything', async () => {
141163
const opts = project({
142164
'pubspec.yaml': 'name: my_flutter_app\nenvironment:\n sdk: ^3.0.0\n',
@@ -169,6 +191,25 @@ describe('android detect', () => {
169191
});
170192
});
171193

194+
describe('go detect', () => {
195+
const detect = GO_AGENT_CONFIG.detection.detect;
196+
197+
test('claims a Go module project', async () => {
198+
const opts = project({ 'go.mod': 'module example.com/app\n\ngo 1.22\n' });
199+
await expect(detect(opts)).resolves.toBe(true);
200+
});
201+
202+
test('does not claim a go.mod without a module directive', async () => {
203+
const opts = project({ 'go.mod': '// not a real module file\n' });
204+
await expect(detect(opts)).resolves.toBe(false);
205+
});
206+
207+
test('does not claim a project without a go.mod', async () => {
208+
const opts = project({ 'main.go': 'package main' });
209+
await expect(detect(opts)).resolves.toBe(false);
210+
});
211+
});
212+
172213
describe('kmp detect', () => {
173214
const detect = KMP_AGENT_CONFIG.detection.detect;
174215

src/lib/detection/__tests__/package-manager.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
composerPackageManager,
88
swiftPackageManager,
99
gradlePackageManager,
10+
goModulesPackageManager,
1011
} from '@lib/detection/package-manager';
1112

1213
vi.mock('../../../utils/debug');
@@ -188,6 +189,7 @@ describe('static package manager helpers', () => {
188189
{ fn: composerPackageManager, name: 'composer' },
189190
{ fn: swiftPackageManager, name: 'spm' },
190191
{ fn: gradlePackageManager, name: 'gradle' },
192+
{ fn: goModulesPackageManager, name: 'go' },
191193
])('$name returns valid PackageManagerInfo', async ({ fn }) => {
192194
const result = await fn();
193195
expect(result.detected).toHaveLength(1);

src/lib/detection/agentic.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,11 @@ export const PROJECT_MANIFESTS: readonly string[] = [
7474
// Ruby / PHP
7575
'Gemfile',
7676
'composer.json',
77-
// Rust / Go / Elixir / JVM / .NET: no framework targets yet, but found so
77+
// Go
78+
'go.mod',
79+
// Rust / Elixir / JVM / .NET: no framework targets yet, but found so
7880
// an existing PostHog SDK is reported (feeds self-driving's "continue" path).
7981
'Cargo.toml',
80-
'go.mod',
8182
'mix.exs',
8283
'pom.xml',
8384
'*.csproj',

src/lib/detection/package-manager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,25 @@ export function bundlerPackageManager(): Promise<PackageManagerInfo> {
215215
});
216216
}
217217

218+
// ---------------------------------------------------------------------------
219+
// Go (modules) helper
220+
// ---------------------------------------------------------------------------
221+
222+
const GO_MODULES: DetectedPackageManager = {
223+
name: 'go',
224+
label: 'Go modules',
225+
installCommand: 'go get',
226+
};
227+
228+
export function goModulesPackageManager(): Promise<PackageManagerInfo> {
229+
return Promise.resolve({
230+
detected: [GO_MODULES],
231+
primary: GO_MODULES,
232+
recommendation:
233+
'Use Go modules (go get). Run go mod tidy after imports change.',
234+
});
235+
}
236+
218237
// ---------------------------------------------------------------------------
219238
// Android (Gradle) helper
220239
// ---------------------------------------------------------------------------

src/lib/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SWIFT_AGENT_CONFIG } from '@frameworks/swift/swift-wizard-agent';
1818
import { KMP_AGENT_CONFIG } from '@frameworks/kmp/kmp-wizard-agent';
1919
import { ANDROID_AGENT_CONFIG } from '@frameworks/android/android-wizard-agent';
2020
import { RAILS_AGENT_CONFIG } from '@frameworks/rails/rails-wizard-agent';
21+
import { GO_AGENT_CONFIG } from '@frameworks/go/go-wizard-agent';
2122
import { PYTHON_AGENT_CONFIG } from '@frameworks/python/python-wizard-agent';
2223
import { RUBY_AGENT_CONFIG } from '@frameworks/ruby/ruby-wizard-agent';
2324
import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@frameworks/javascript-node/javascript-node-wizard-agent';
@@ -42,6 +43,7 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
4243
[Integration.swift]: SWIFT_AGENT_CONFIG,
4344
[Integration.android]: ANDROID_AGENT_CONFIG,
4445
[Integration.rails]: RAILS_AGENT_CONFIG,
46+
[Integration.go]: GO_AGENT_CONFIG,
4547
[Integration.python]: PYTHON_AGENT_CONFIG,
4648
[Integration.ruby]: RUBY_AGENT_CONFIG,
4749
[Integration.javascriptNode]: JAVASCRIPT_NODE_AGENT_CONFIG,

0 commit comments

Comments
 (0)