Skip to content

Commit 310d0db

Browse files
authored
fix: missing cmf-demo main.go file and gitignore fix that missed it (#52)
Co-authored-by: Teryl Taylor <terylt@ibm.com>
1 parent c1659fb commit 310d0db

3 files changed

Lines changed: 297 additions & 13 deletions

File tree

.gitignore

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ token.txt
2929
cpex.sbom.xml
3030
docs/docs/test/
3131
docs/resources/
32-
tmp
3332
*.tgz
3433
*.gz
3534
*.bz
@@ -48,8 +47,10 @@ node_modules/
4847
mcp.db-journal
4948
mcp.db-shm
5049
mcp.db-wal
51-
certs/
52-
jwt/
50+
# Anchored: matches only ./certs/ at the repo root, not nested
51+
# `certs/` directories under source. Bare `certs/` would silently
52+
# hide any cert-management module / test fixture at any depth.
53+
/certs/
5354
FIXMEs
5455
*.old
5556
logs/
@@ -62,19 +63,22 @@ corpus/
6263
tests/fuzz/fuzzers/results/
6364
.venv
6465
mcp.db
65-
public/
66+
# Anchored to repo root — bare `public/` would shadow any nested
67+
# `public/` directory in source (common in web/frontend code).
68+
/public/
6669
ica_integrations_host.sbom.json
6770
.pyre
6871
dictionary.dic
6972
pdm.lock
7073
.pdm-python
7174
temp/
72-
public/
7375
*history.md
7476
htmlcov
7577
test_commands.md
7678
cover.md
77-
build/
79+
# Anchored: bare `build/` would shadow any nested build-output dir
80+
# anywhere in the source tree.
81+
/build/
7882
.icaenv
7983
commands_output.txt
8084
commands_output.md
@@ -94,7 +98,6 @@ scribeflow.log
9498
coverage_re
9599
bin/flagged
96100
flagged/
97-
certs/
98101
# VENV
99102
.python37/
100103
.python39/
@@ -111,16 +114,20 @@ __pycache__/
111114
# C extensions
112115
*.so
113116

114-
# Distribution / packaging
117+
# Distribution / packaging — Python build artifacts. `build/` and
118+
# `lib/` are anchored (root-only) so they don't silently hide
119+
# nested source dirs of the same name. Other patterns (`dist/`,
120+
# `downloads/`, `eggs/`, …) stay bare — they're less likely to
121+
# collide with source-tree directory names.
115122
.wily/
116123
.Python
117-
build/
118124
develop-eggs/
119125
dist/
120126
downloads/
121127
eggs/
122128
.eggs/
123-
lib/
129+
# Anchored: bare `lib/` would shadow any nested `lib/` source dir.
130+
/lib/
124131
lib64/
125132
parts/
126133
sdist/

examples/go-demo/.gitignore

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
# Built demo binaries
2-
cpex-demo
3-
cmf-demo
1+
# Built demo binaries. Patterns are anchored (leading slash) so they
2+
# match *files* at their build-output locations, not arbitrary path
3+
# components — the previous unanchored `cmf-demo` rule was silently
4+
# ignoring the `cmd/cmf-demo/` source directory and everything under
5+
# it.
6+
/cpex-demo
7+
/cmf-demo
8+
/cmd/cmf-demo/cmf-demo
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Location: ./examples/go-demo/cmd/cmf-demo/main.go
2+
// Copyright 2025
3+
// SPDX-License-Identifier: Apache-2.0
4+
// Authors: Teryl Taylor
5+
//
6+
// CPEX CMF Demo — typed message processing with rich extensions.
7+
//
8+
// Demonstrates CMF (ContextForge Message Format) message processing
9+
// through the CPEX plugin pipeline:
10+
//
11+
// 1. Build typed CMF messages (tool calls, tool results)
12+
// 2. Attach security extensions (labels, subject), HTTP headers,
13+
// and agent context
14+
// 3. Invoke cmf.tool_pre_invoke — policy checks tool permissions
15+
// against security labels and meta tags
16+
// 4. Invoke cmf.tool_post_invoke — header injector adds response
17+
// headers using capability-gated write access
18+
// 5. Inspect modified extensions (injected headers) in results
19+
//
20+
// Build & run:
21+
//
22+
// cd examples/go-demo/ffi && cargo build --release
23+
// cd examples/go-demo && go run ./cmd/cmf-demo
24+
25+
package main
26+
27+
/*
28+
#cgo LDFLAGS: -L${SRCDIR}/../../../../target/release -lcpex_demo_ffi -lm -ldl -lpthread -framework CoreFoundation -framework Security
29+
#include <stdlib.h>
30+
31+
int cpex_demo_register_factories(void* mgr);
32+
*/
33+
import "C"
34+
35+
import (
36+
"fmt"
37+
"os"
38+
"unsafe"
39+
40+
cpex "github.com/contextforge-org/contextforge-plugins-framework/go/cpex"
41+
)
42+
43+
func main() {
44+
fmt.Println("=== CPEX CMF Demo ===")
45+
fmt.Println()
46+
47+
// --- Setup ---
48+
mgr, err := cpex.NewPluginManagerDefault()
49+
if err != nil {
50+
fatal("create manager: %v", err)
51+
}
52+
defer mgr.Shutdown()
53+
54+
err = mgr.RegisterFactories(func(handle unsafe.Pointer) error {
55+
if C.cpex_demo_register_factories(handle) != 0 {
56+
return fmt.Errorf("factory registration failed")
57+
}
58+
return nil
59+
})
60+
if err != nil {
61+
fatal("register factories: %v", err)
62+
}
63+
64+
yaml, err := os.ReadFile("../../cmf_plugins.yaml")
65+
if err != nil {
66+
// Try current directory too
67+
yaml, err = os.ReadFile("cmf_plugins.yaml")
68+
if err != nil {
69+
fatal("read config: %v", err)
70+
}
71+
}
72+
73+
if err := mgr.LoadConfig(string(yaml)); err != nil {
74+
fatal("load config: %v", err)
75+
}
76+
if err := mgr.Initialize(); err != nil {
77+
fatal("initialize: %v", err)
78+
}
79+
80+
fmt.Printf("Plugins loaded: %d\n", mgr.PluginCount())
81+
fmt.Printf("Hooks: cmf.tool_pre_invoke=%v cmf.tool_post_invoke=%v\n\n",
82+
mgr.HasHooksFor("cmf.tool_pre_invoke"),
83+
mgr.HasHooksFor("cmf.tool_post_invoke"),
84+
)
85+
86+
// -----------------------------------------------------------------------
87+
// Scenario 1: PII tool call WITHOUT security label — DENIED
88+
// -----------------------------------------------------------------------
89+
fmt.Println("=== Scenario 1: get_compensation tool call (no PII label) ===")
90+
fmt.Println()
91+
92+
msg := cpex.MessagePayload{
93+
Message: cpex.NewMessage("assistant",
94+
cpex.NewTextPart("I'll look up the compensation data for you."),
95+
cpex.NewToolCallPart(cpex.ToolCall{
96+
ToolCallID: "tc_001",
97+
Name: "get_compensation",
98+
Arguments: map[string]any{"employee_id": 42},
99+
Namespace: "hr",
100+
}),
101+
),
102+
}
103+
104+
ext := &cpex.Extensions{
105+
Meta: &cpex.MetaExtension{
106+
EntityType: "tool",
107+
EntityName: "get_compensation",
108+
Tags: []string{"pii", "hr"},
109+
},
110+
Security: &cpex.SecurityExtension{
111+
Labels: []string{}, // no PII label — should be denied
112+
Subject: &cpex.SubjectExtension{
113+
ID: "alice",
114+
Roles: []string{"hr_analyst"},
115+
},
116+
},
117+
Http: &cpex.HttpExtension{
118+
RequestHeaders: map[string]string{
119+
"Authorization": "Bearer eyJ...",
120+
"X-Request-ID": "req-001",
121+
},
122+
},
123+
Agent: &cpex.AgentExtension{
124+
SessionID: "sess_abc123",
125+
AgentID: "hr-assistant",
126+
},
127+
}
128+
129+
result, ct, bg, err := mgr.InvokeByName("cmf.tool_pre_invoke",
130+
cpex.PayloadCMFMessage, msg, ext, nil)
131+
if err != nil {
132+
fatal("invoke: %v", err)
133+
}
134+
printResult(result)
135+
bg.Close()
136+
ct.Close()
137+
138+
// -----------------------------------------------------------------------
139+
// Scenario 2: PII tool call WITH security label — ALLOWED
140+
// -----------------------------------------------------------------------
141+
fmt.Println("=== Scenario 2: get_compensation tool call (with PII label) ===")
142+
fmt.Println()
143+
144+
ext.Security.Labels = []string{"PII", "HR"} // now has PII label
145+
146+
result, ct, bg, err = mgr.InvokeByName("cmf.tool_pre_invoke",
147+
cpex.PayloadCMFMessage, msg, ext, nil)
148+
if err != nil {
149+
fatal("invoke: %v", err)
150+
}
151+
printResult(result)
152+
153+
// Check for modified extensions (header injector adds response headers)
154+
// Check for modified extensions (header injector adds response headers)
155+
if len(result.ModifiedExtensions) > 0 {
156+
modExt, err := result.DeserializeExtensions()
157+
if err != nil {
158+
fmt.Printf(" (failed to deserialize modified extensions: %v)\n\n", err)
159+
} else if modExt != nil && modExt.Http != nil && len(modExt.Http.ResponseHeaders) > 0 {
160+
fmt.Println(" Modified response headers:")
161+
for k, v := range modExt.Http.ResponseHeaders {
162+
fmt.Printf(" %s: %s\n", k, v)
163+
}
164+
fmt.Println()
165+
}
166+
}
167+
bg.Close()
168+
169+
// -----------------------------------------------------------------------
170+
// Scenario 3: Post-invoke with tool result — header injection
171+
// -----------------------------------------------------------------------
172+
fmt.Println("=== Scenario 3: tool result post-invoke (header injection) ===")
173+
fmt.Println()
174+
175+
resultMsg := cpex.MessagePayload{
176+
Message: cpex.NewMessage("tool",
177+
cpex.NewToolResultPart(cpex.ToolResult{
178+
ToolCallID: "tc_001",
179+
ToolName: "get_compensation",
180+
Content: map[string]any{
181+
"employee_id": 42,
182+
"salary": 125000,
183+
"currency": "USD",
184+
},
185+
IsError: false,
186+
}),
187+
),
188+
}
189+
190+
postExt := &cpex.Extensions{
191+
Meta: &cpex.MetaExtension{
192+
EntityType: "tool",
193+
EntityName: "get_compensation",
194+
Tags: []string{"pii", "hr"},
195+
},
196+
Security: &cpex.SecurityExtension{
197+
Labels: []string{"PII", "HR"},
198+
},
199+
Http: &cpex.HttpExtension{
200+
RequestHeaders: map[string]string{
201+
"Authorization": "Bearer eyJ...",
202+
"X-Request-ID": "req-001",
203+
},
204+
},
205+
}
206+
207+
result2, ct2, bg2, err := mgr.InvokeByName("cmf.tool_post_invoke",
208+
cpex.PayloadCMFMessage, resultMsg, postExt, ct)
209+
if err != nil {
210+
fatal("post-invoke: %v", err)
211+
}
212+
printResult(result2)
213+
214+
if len(result2.ModifiedExtensions) > 0 {
215+
modExt, err := result2.DeserializeExtensions()
216+
if err != nil {
217+
fmt.Printf(" (failed to deserialize modified extensions: %v)\n\n", err)
218+
} else if modExt != nil && modExt.Http != nil {
219+
fmt.Println(" Modified response headers:")
220+
for k, v := range modExt.Http.ResponseHeaders {
221+
fmt.Printf(" %s: %s\n", k, v)
222+
}
223+
fmt.Println()
224+
}
225+
}
226+
bg2.Close()
227+
ct2.Close()
228+
229+
// -----------------------------------------------------------------------
230+
// Scenario 4: Non-PII tool — allowed, no policy restriction
231+
// -----------------------------------------------------------------------
232+
fmt.Println("=== Scenario 4: list_departments (non-PII, text message) ===")
233+
fmt.Println()
234+
235+
textMsg := cpex.MessagePayload{
236+
Message: cpex.NewMessage("user",
237+
cpex.NewTextPart("Show me the list of departments"),
238+
),
239+
}
240+
241+
textExt := &cpex.Extensions{
242+
Meta: &cpex.MetaExtension{
243+
EntityType: "tool",
244+
EntityName: "list_departments",
245+
},
246+
}
247+
248+
result, ct, bg, err = mgr.InvokeByName("cmf.tool_pre_invoke",
249+
cpex.PayloadCMFMessage, textMsg, textExt, nil)
250+
if err != nil {
251+
fatal("invoke: %v", err)
252+
}
253+
printResult(result)
254+
bg.Close()
255+
ct.Close()
256+
257+
fmt.Println("=== CMF Demo complete ===")
258+
}
259+
260+
func printResult(result *cpex.PipelineResult) {
261+
if !result.IsDenied() {
262+
fmt.Printf(" Result: ALLOWED\n\n")
263+
} else {
264+
v := result.Violation
265+
fmt.Printf(" Result: DENIED — %s [%s]\n\n", v.Reason, v.Code)
266+
}
267+
}
268+
269+
func fatal(format string, args ...any) {
270+
fmt.Fprintf(os.Stderr, "ERROR: "+format+"\n", args...)
271+
os.Exit(1)
272+
}

0 commit comments

Comments
 (0)