sage-governance 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.MD CHANGED
@@ -448,7 +448,15 @@ report_generate(session_id?, output_format?)
448
448
  → {content: string, report_path: string}
449
449
  → output_format: "markdown" (full model card) | "summary" (terminal)
450
450
  → Call at end of session or on developer request
451
- ```
451
+
452
+ duckduckgo-search (via ddg server)
453
+ → Query for real-time web searches and factual documentation retrieval.
454
+ → Use ONLY when current state, external documentation, or real-time web facts are needed.
455
+
456
+ github (via github server)
457
+ → Query for git commands, repository status, commits, and pull requests.
458
+ → Use ONLY when interacting with GitHub or performing git/PR tasks.
459
+ ```\n\n## MCP Tool Integration\n\nThe SAGE runtime can combine the generic web‑search (`duckduckgo-search`) and GitHub (`github`) MCP tools with the core SAGE MCP tools to enrich evaluations.\n\n**When to use:**\n- **`duckduckgo-search`** – before calling `sage_evaluate` when the prompt references statutes, standards, or recent policy updates that need factual verification.\n- **`github`** – when the task involves code provenance, repository status, or pulling commit metadata (e.g., verifying that a referenced file exists in the repo).\n\n**How to combine:**\n1. Run `duckduckgo-search` (or `github`) and capture the result.\n2. Include the retrieved information in the `context` argument of `sage_evaluate`.\n3. If the result contains commit hashes or issue identifiers, log them via `audit_write` for traceability.\n4. Continue with the normal SAGE flow (`sage_evaluate` → optional fairness options → `intercept_file_write`).\n\n**Example flow (Markdown mermaid):**\n```mermaid\nflowchart TD\n A[Start] --> B[duckduckgo-search / github]\n B --> C[Capture result]\n C --> D[sage_evaluate(prompt, context=result)]\n D --> E{Risk level}\n E -->|LOW/MEDIUM| F[Proceed to coding]\n E -->|HIGH/CRITICAL| G[Present fairness options]\n F --> H[intercept_file_write]\n G --> H\n H --> I[audit_write]\n I --> J[Done]\n```\n\n*All findings from these external calls are recorded in the audit trail and are subject to the same HITL requirements as other SAGE decisions.*\n
452
460
 
453
461
  ---
454
462
 
@@ -0,0 +1,9 @@
1
+ # Contributors
2
+
3
+ SAGE is maintained by Team SAGE.
4
+
5
+ - George Mihaileanu
6
+ - Prajwal Srinivas
7
+ - Olu Akinnawo
8
+ - Roshan Sharma
9
+ - Jeremy
package/bin/sage.js CHANGED
@@ -7,20 +7,21 @@
7
7
  * SAGE MCP server (sage/mcp_server.py).
8
8
  */
9
9
 
10
- const { spawn } = require('child_process');
10
+ const { spawn, spawnSync } = require('child_process');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
+ const os = require('os');
13
14
 
14
15
  const PROJECT_ROOT = path.resolve(__dirname, '..');
15
16
  const MCP_SERVER_PATH = path.join(PROJECT_ROOT, 'sage', 'mcp_server.py');
17
+ const BOOTSTRAP_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'bootstrap-python.js');
16
18
 
17
19
  /**
18
20
  * Resolves the Python executable in order of preference.
19
21
  */
20
22
  function getPythonExecutable() {
21
- const executables = ['python3', 'python', 'py'];
23
+ const executables = process.platform === 'win32' ? ['python', 'python3', 'py'] : ['python3', 'python'];
22
24
  const { execSync } = require('child_process');
23
-
24
25
  for (const exe of executables) {
25
26
  try {
26
27
  execSync(`${exe} --version`, { stdio: 'ignore' });
@@ -32,28 +33,68 @@ function getPythonExecutable() {
32
33
  return null;
33
34
  }
34
35
 
35
- const pythonExe = getPythonExecutable();
36
+ // Resolve Python executable, preferring bundled venv if available
37
+ let pythonExe = getPythonExecutable();
38
+ const venvDir = path.join(os.homedir(), '.cache', 'sage-governance', 'venv');
39
+ const venvPython = process.platform === 'win32'
40
+ ? path.join(venvDir, 'Scripts', 'python.exe')
41
+ : path.join(venvDir, 'bin', 'python');
42
+ if (fs.existsSync(venvPython)) {
43
+ pythonExe = venvPython;
44
+ }
36
45
 
37
- if (!pythonExe) {
38
- console.error('[SAGE] Error: Python 3 not found in PATH.');
39
- console.error(' Please install Python 3 (https://python.org) and try again.');
40
- process.exit(1);
46
+ /**
47
+ * Run the bootstrap script synchronously. Used when the user explicitly
48
+ * requests `sage setup` or when the venv is missing before starting the server.
49
+ */
50
+ function runBootstrap() {
51
+ console.log('[SAGE] Running bootstrap to prepare Python environment…');
52
+ const result = spawnSync('node', [BOOTSTRAP_SCRIPT], { stdio: 'inherit' });
53
+ if (result.status !== 0) {
54
+ console.error('[SAGE] Bootstrap failed.');
55
+ process.exit(result.status || 1);
56
+ }
57
+ console.log('[SAGE] Bootstrap completed successfully.');
41
58
  }
42
59
 
43
- // Check if running interactively or if help/setup/config argument is passed
44
- const isTTY = process.stdin.isTTY || process.stdout.isTTY;
45
- const hasHelpOrSetup = process.argv.some(arg =>
46
- ['--help', '-h', 'help', 'setup', '--setup', 'config', '--config'].includes(arg.toLowerCase())
47
- );
60
+ // Determine command mode ----------------------------------------------------
61
+ const args = process.argv.slice(2);
62
+ const firstArg = args[0] ? args[0].toLowerCase() : '';
63
+
64
+ // Help / setup flags -------------------------------------------------------
65
+ const isHelp = ['--help', '-h', 'help'].includes(firstArg);
66
+ const isSetup = ['setup', '--setup', 'config', '--config'].includes(firstArg);
48
67
 
49
- if (isTTY || hasHelpOrSetup) {
68
+ if (isHelp) {
69
+ // Show the user‑facing guide (same as before) and exit.
50
70
  printSetupGuide();
51
71
  process.exit(0);
52
72
  }
53
73
 
54
- const child = spawn(pythonExe, [MCP_SERVER_PATH, ...process.argv.slice(2)], {
74
+ if (isSetup) {
75
+ // Explicit bootstrap request.
76
+ runBootstrap();
77
+ process.exit(0);
78
+ }
79
+
80
+ // If the venv is missing, auto‑bootstrap before launching the server.
81
+ if (!fs.existsSync(venvPython)) {
82
+ console.log('[SAGE] No bundled virtual environment detected – bootstrapping now.');
83
+ runBootstrap();
84
+ // Refresh pythonExe after bootstrap.
85
+ pythonExe = venvPython;
86
+ }
87
+
88
+ if (!pythonExe) {
89
+ console.error('[SAGE] Error: Python 3 not found in PATH and no bundled venv detected.');
90
+ console.error(' Please ensure Python 3 is installed or run `sage setup` to bootstrap.');
91
+ process.exit(1);
92
+ }
93
+
94
+ // Launch the MCP server ----------------------------------------------------
95
+ const child = spawn(pythonExe, [MCP_SERVER_PATH, ...args], {
55
96
  stdio: 'inherit',
56
- env: process.env
97
+ env: process.env,
57
98
  });
58
99
 
59
100
  child.on('error', (err) => {
@@ -66,79 +107,11 @@ child.on('exit', (code) => {
66
107
  });
67
108
 
68
109
  function printSetupGuide() {
69
- console.log(`
70
- \x1b[1;36m╔══════════════════════════════════════════════════════════════════════════╗\x1b[0m
110
+ console.log(`\n\x1b[1;36m╔══════════════════════════════════════════════════════════════════════════╗\x1b[0m
71
111
  \x1b[1;36m║ SAGE — Supervisory Agentic Governance Engine ║\x1b[0m
72
112
  \x1b[1;36m║ Model Context Protocol (MCP) Server Setup & Configuration Guide ║\x1b[0m
73
113
  \x1b[1;36m╚══════════════════════════════════════════════════════════════════════════╝\x1b[0m
74
114
 
75
- SAGE is designed to run as an MCP server inside your favorite AI coding environment.
76
- Since you ran SAGE in an interactive terminal, here is how to configure it.
77
-
78
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
79
- \x1b[1;32m1. CURSOR CONFIGURATION\x1b[0m
80
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
81
- Go to: \x1b[1mCursor Settings -> Features -> MCP -> Add New MCP Server\x1b[0m
82
-
83
- • \x1b[1mName:\x1b[0m sage-governance
84
- • \x1b[1mType:\x1b[0m command
85
- • \x1b[1mCommand:\x1b[0m sage
86
- • \x1b[1mArgs:\x1b[0m (leave empty)
87
-
88
- Alternatively, add the following to \x1b[34m.cursor/mcp.json\x1b[0m in your project:
89
-
90
- \x1b[32m{
91
- "mcpServers": {
92
- "sage-governance": {
93
- "command": "sage",
94
- "args": [],
95
- "type": "stdio",
96
- "env": {
97
- "OPENAI_API_KEY": "YOUR_OPENAI_API_KEY",
98
- "SAGE_LLM_MODEL": "gpt-4o-mini"
99
- }
100
- }
101
- }
102
- }\x1b[0m
103
-
104
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
105
- \x1b[1;32m2. OPENCODE CONFIGURATION\x1b[0m
106
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
107
- Add the following to \x1b[34mopencode.json\x1b[0m in your project root directory:
108
-
109
- \x1b[32m{
110
- "mcp": {
111
- "sage-governance": {
112
- "type": "local",
113
- "command": ["sage"],
114
- "enabled": true,
115
- "environment": {
116
- "OPENAI_API_KEY": "(env:OPENAI_API_KEY)",
117
- "SAGE_LLM_MODEL": "gpt-4o-mini"
118
- }
119
- }
120
- }
121
- }\x1b[0m
122
-
123
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
124
- \x1b[1;32m3. CLAUDE DESKTOP CONFIGURATION\x1b[0m
125
- \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
126
- Add the following to your global Claude Desktop configuration:
127
- Path: \x1b[34m~/Library/Application Support/Claude/claude_desktop_config.json\x1b[0m
128
-
129
- \x1b[32m{
130
- "mcpServers": {
131
- "sage-governance": {
132
- "command": "sage",
133
- "args": [],
134
- "env": {
135
- "OPENAI_API_KEY": "YOUR_OPENAI_API_KEY",
136
- "SAGE_LLM_MODEL": "gpt-4o-mini"
137
- }
138
- }
139
- }
140
- }\x1b[0m
141
-
142
115
  \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
143
116
  \x1b[1;32m4. CLAUDE CODE CONFIGURATION\x1b[0m
144
117
  \x1b[1;33m────────────────────────────────────────────────────────────────────────────\x1b[0m
package/claude.json CHANGED
@@ -11,6 +11,29 @@
11
11
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
12
12
  "SAGE_LLM_MODEL": "gpt-4o-mini"
13
13
  }
14
- }
14
+ },
15
+ "duckduckgo-search": {
16
+ "type": "local",
17
+ "command": ["uvx", "duckduckgo-mcp-server"],
18
+ "enabled": true,
19
+ "timeout": 10000
20
+ },
21
+ "github": {
22
+ "type": "local",
23
+ "command": [
24
+ "docker",
25
+ "run",
26
+ "-i",
27
+ "--rm",
28
+ "-e",
29
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
30
+ "ghcr.io/github/github-mcp-server"
31
+ ],
32
+ "enabled": false,
33
+ "environment": {
34
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
35
+ },
36
+ "timeout": 10000
15
37
  }
16
38
  }
39
+ }
package/codex.json CHANGED
@@ -13,6 +13,30 @@
13
13
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
14
14
  "SAGE_LLM_MODEL": "gpt-4o-mini"
15
15
  },
16
+ "duckduckgo-search": {
17
+ "type": "local",
18
+ "command": ["uvx", "duckduckgo-mcp-server"],
19
+ "enabled": true,
20
+ "timeout": 10000
21
+ },
22
+ "github": {
23
+ "type": "local",
24
+ "command": [
25
+ "docker",
26
+ "run",
27
+ "-i",
28
+ "--rm",
29
+ "-e",
30
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
31
+ "ghcr.io/github/github-mcp-server"
32
+ ],
33
+ "enabled": false,
34
+ "environment": {
35
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
36
+ },
37
+ "timeout": 10000
38
+ }
39
+ ,
16
40
  "description": "SAGE governance layer — evaluates ethics, fairness, and regulatory compliance before code generation"
17
41
  }
18
42
  },
package/cursor.json CHANGED
@@ -21,6 +21,29 @@
21
21
  "env": {
22
22
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
23
23
  "SAGE_LLM_MODEL": "gpt-4o-mini"
24
+ },
25
+ "duckduckgo-search": {
26
+ "type": "local",
27
+ "command": ["uvx", "duckduckgo-mcp-server"],
28
+ "enabled": true,
29
+ "timeout": 10000
30
+ },
31
+ "github": {
32
+ "type": "local",
33
+ "command": [
34
+ "docker",
35
+ "run",
36
+ "-i",
37
+ "--rm",
38
+ "-e",
39
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
40
+ "ghcr.io/github/github-mcp-server"
41
+ ],
42
+ "enabled": false,
43
+ "environment": {
44
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
45
+ },
46
+ "timeout": 10000
24
47
  }
25
48
  }
26
49
  }
package/kimicode.json CHANGED
@@ -10,6 +10,29 @@
10
10
  "env": {
11
11
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
12
12
  "SAGE_LLM_MODEL": "gpt-4o-mini"
13
+ },
14
+ "duckduckgo-search": {
15
+ "type": "local",
16
+ "command": ["uvx", "duckduckgo-mcp-server"],
17
+ "enabled": true,
18
+ "timeout": 10000
19
+ },
20
+ "github": {
21
+ "type": "local",
22
+ "command": [
23
+ "docker",
24
+ "run",
25
+ "-i",
26
+ "--rm",
27
+ "-e",
28
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
29
+ "ghcr.io/github/github-mcp-server"
30
+ ],
31
+ "enabled": false,
32
+ "environment": {
33
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
34
+ },
35
+ "timeout": 10000
13
36
  }
14
37
  }
15
38
  }
package/opencode.json CHANGED
@@ -9,6 +9,29 @@
9
9
  "environment": {
10
10
  "OPENAI_API_KEY": "(env:OPENAI_API_KEY)",
11
11
  "SAGE_LLM_MODEL": "gpt-4o-mini"
12
+ },
13
+ "duckduckgo-search": {
14
+ "type": "local",
15
+ "command": ["uvx", "duckduckgo-mcp-server"],
16
+ "enabled": true,
17
+ "timeout": 10000
18
+ },
19
+ "github": {
20
+ "type": "local",
21
+ "command": [
22
+ "docker",
23
+ "run",
24
+ "-i",
25
+ "--rm",
26
+ "-e",
27
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
28
+ "ghcr.io/github/github-mcp-server"
29
+ ],
30
+ "enabled": false,
31
+ "environment": {
32
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
33
+ },
34
+ "timeout": 10000
12
35
  }
13
36
  }
14
37
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sage-governance",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Supervisory Agentic Governance Engine — Open-source MCP governance layer for agentic coding systems. Intercepts, evaluates, and audits AI coding prompts for EU AI Act, GDPR, and fairness compliance.",
5
5
  "main": "bin/sage.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node bin/sage.js",
11
11
  "prepublishOnly": "find . -path './.git' -prune -o -name '__pycache__' -type d -print -exec rm -rf {} + 2>/dev/null; echo 'pycache cleaned'",
12
- "postinstall": "node -e \"console.log('\\n[SAGE] Python deps required — run: pip install -r requirements.txt\\n')\""
12
+ "postinstall": "node scripts/bootstrap-python.js"
13
13
  },
14
14
  "files": [
15
15
  "bin/",
@@ -26,7 +26,10 @@
26
26
  "codex.json",
27
27
  "windsurf.json",
28
28
  "trae.json",
29
- "kimicode.json"
29
+ "kimicode.json",
30
+ "Contributors.md",
31
+ "security.md",
32
+ "scripts/"
30
33
  ],
31
34
  "keywords": [
32
35
  "mcp",
@@ -44,7 +47,7 @@
44
47
  "responsible-ai",
45
48
  "model-card"
46
49
  ],
47
- "author": "SAGE Team <team@olustar.io>",
50
+ "author": "SAGE Team",
48
51
  "license": "MIT",
49
52
  "repository": {
50
53
  "type": "git",
@@ -58,4 +61,4 @@
58
61
  "engines": {
59
62
  "node": ">=16.0.0"
60
63
  }
61
- }
64
+ }
@@ -28,6 +28,7 @@ import sys
28
28
  from typing import Any, Literal, Optional
29
29
 
30
30
  from pydantic import BaseModel, field_validator
31
+ __all__: list[str] = []
31
32
 
32
33
  # startup is always imported first — it pre-loads all globals
33
34
  from startup import (
@@ -37,6 +38,7 @@ from startup import (
37
38
  LLM_CLIENT,
38
39
  LLM_MODEL,
39
40
  POLICY_INDEX,
41
+ POLICY_DOCS,
40
42
  PROTECTED_ATTRIBUTES,
41
43
  PROXY_ATTRIBUTE_MAP,
42
44
  UDHR_ARTICLE_MAP,
@@ -597,13 +599,24 @@ def _enrich_with_llm(prompt: str, det: dict[str, Any]) -> str:
597
599
  if not LLM_AVAILABLE:
598
600
  return fallback
599
601
 
602
+ domain_info = POLICY_INDEX.get(det['domain'], POLICY_INDEX.get('general', {}))
603
+ policy_files = domain_info.get('files', [])
604
+ policy_texts = []
605
+ for pf in policy_files:
606
+ if pf in POLICY_DOCS:
607
+ # Inject up to 2500 chars per policy to ensure context fits
608
+ policy_texts.append(f"--- {pf} ---\n{POLICY_DOCS[pf][:2500]}")
609
+
610
+ policies_context = "\n\n".join(policy_texts)
611
+
600
612
  system = (
601
613
  "You are SAGE, a governance agent for AI systems. Given a developer's coding "
602
614
  "prompt and a preliminary risk classification, write a concise 2-3 sentence "
603
615
  "explanation of WHY this prompt raises ethical or regulatory concerns. "
604
- "Reference specific laws, principles, or documented real-world cases. "
616
+ "Reference specific laws, principles, or documented real-world cases from the provided policy documents. "
605
617
  "Be factual and precise. Output ONLY the explanation — no JSON, no preamble, "
606
- "no bullet points."
618
+ "no bullet points.\n\n"
619
+ f"RELEVANT POLICIES:\n{policies_context}"
607
620
  )
608
621
  user = (
609
622
  f"Developer prompt: \"{prompt}\"\n\n"
@@ -619,7 +632,7 @@ def _enrich_with_llm(prompt: str, det: dict[str, Any]) -> str:
619
632
  try:
620
633
  response = LLM_CLIENT.chat.completions.create(
621
634
  model=LLM_MODEL,
622
- max_tokens=350,
635
+ max_tokens=1500,
623
636
  messages=[
624
637
  {"role": "system", "content": system},
625
638
  {"role": "user", "content": user},
@@ -708,3 +721,35 @@ def log_model_metrics(metrics: dict[str, Any], dataset_info: Optional[dict[str,
708
721
  "dataset_info": dataset_info or {},
709
722
  }
710
723
  return write_audit_entry(entry)
724
+
725
+ # ----- Added utilities to activate previously unused imports and variables -----
726
+
727
+ if FAIRLEARN_AVAILABLE:
728
+ # expose Fairlearn metrics when available
729
+ from fairlearn.metrics import demographic_parity_difference, equalized_odds_difference, MetricFrame
730
+ __all__ += ["demographic_parity_difference", "equalized_odds_difference", "MetricFrame"]
731
+ else:
732
+ # placeholder functions raising informative errors
733
+ def _fairlearn_unavailable(*_args, **_kwargs):
734
+ raise ImportError("Fairlearn is not installed. Install it to use fairness metrics.")
735
+ demographic_parity_difference = _fairlearn_unavailable
736
+ equalized_odds_difference = _fairlearn_unavailable
737
+ MetricFrame = _fairlearn_unavailable
738
+
739
+ def list_fairness_options() -> dict[str, "FairnessOption"]:
740
+ """Return a dictionary of all defined fairness options.
741
+ This makes the previously defined _FAIRNESS_LIBRARY actively used.
742
+ """
743
+ return _FAIRNESS_LIBRARY.copy()
744
+
745
+ def get_fairness_option(name: str) -> "FairnessOption":
746
+ """Retrieve a specific FairnessOption by name.
747
+ Raises KeyError if the option does not exist.
748
+ """
749
+ try:
750
+ return _FAIRNESS_LIBRARY[name]
751
+ except KeyError as exc:
752
+ raise KeyError(f"Fairness option '{name}' not found. Available options: {list(_FAIRNESS_LIBRARY.keys())}") from exc
753
+
754
+ # Ensure the new utilities are part of the module's public API
755
+ __all__ += ["list_fairness_options", "get_fairness_option"]
package/sage/startup.py CHANGED
@@ -28,11 +28,13 @@ from typing import Any
28
28
  _THIS_FILE = pathlib.Path(__file__).resolve()
29
29
  PROJECT_ROOT = _THIS_FILE.parent.parent
30
30
 
31
+ WORKSPACE_ROOT = pathlib.Path(os.getcwd()).resolve()
32
+
31
33
  RULES_DIR = PROJECT_ROOT / "rules"
32
- AUDIT_FILE = PROJECT_ROOT / "audit-trail" / "decisions.jsonl"
33
- LOGS_FILE = PROJECT_ROOT / "LOGS.md"
34
- LOCAL_MEMORY = PROJECT_ROOT / "local_memory.md"
35
- REPORTS_DIR = PROJECT_ROOT / "reports"
34
+ AUDIT_FILE = WORKSPACE_ROOT / "audit-trail" / "decisions.jsonl"
35
+ LOGS_FILE = WORKSPACE_ROOT / "LOGS.md"
36
+ LOCAL_MEMORY = WORKSPACE_ROOT / "local_memory.md"
37
+ REPORTS_DIR = WORKSPACE_ROOT / "reports"
36
38
 
37
39
  # ── Ensure required dirs & files exist ───────────────────────────────────────
38
40
  for _p in (AUDIT_FILE.parent, REPORTS_DIR):
@@ -62,8 +64,9 @@ def write_audit_entry(entry: dict) -> str:
62
64
  prev_hash = last_entry.get("entry_hash", "")
63
65
  if not session_id:
64
66
  session_id = last_entry.get("session_id")
65
- except Exception:
66
- pass
67
+ except Exception as e:
68
+ import sys
69
+ print(f"[SAGE] Error reading audit file for chain hash: {e}", file=sys.stderr)
67
70
 
68
71
  if not session_id:
69
72
  session_id = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // scripts/bootstrap-python.js
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Runs automatically on `npm install -g sage-governance` (postinstall hook).
5
+ // Creates a private Python virtual environment and installs requirements.
6
+ // Re-installs only when requirements.txt has changed (hash-checked).
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ const { spawnSync } = require('child_process');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const crypto = require('crypto');
14
+
15
+ const root = path.resolve(__dirname, '..');
16
+ const reqPath = path.join(root, 'requirements.txt');
17
+
18
+ // Private venv lives in ~/.cache/sage-governance/venv (cross-platform friendly)
19
+ const cacheDir = path.join(os.homedir(), '.cache', 'sage-governance');
20
+ const venvDir = path.join(cacheDir, 'venv');
21
+ const lockFile = path.join(cacheDir, 'requirements.lock');
22
+
23
+ // ── helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function banner(msg) {
26
+ console.log(`\x1b[36m[SAGE bootstrap]\x1b[0m ${msg}`);
27
+ }
28
+
29
+ function success(msg) {
30
+ console.log(`\x1b[32m[SAGE bootstrap] ✔\x1b[0m ${msg}`);
31
+ }
32
+
33
+ function warn(msg) {
34
+ console.warn(`\x1b[33m[SAGE bootstrap] ⚠\x1b[0m ${msg}`);
35
+ }
36
+
37
+ function fatal(msg) {
38
+ console.error(`\x1b[31m[SAGE bootstrap] ✖\x1b[0m ${msg}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ function run(cmd, args, opts = {}) {
43
+ const result = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
44
+ if (result.error) fatal(`Failed to run '${cmd}': ${result.error.message}`);
45
+ if (result.status !== 0) fatal(`'${cmd} ${args.join(' ')}' exited with code ${result.status}`);
46
+ }
47
+
48
+ // ── locate Python 3 ──────────────────────────────────────────────────────────
49
+
50
+ function findPython3() {
51
+ const candidates = process.platform === 'win32'
52
+ ? ['python', 'python3', 'py']
53
+ : ['python3', 'python'];
54
+
55
+ for (const exe of candidates) {
56
+ const res = spawnSync(exe, ['--version'], { stdio: 'pipe' });
57
+ if (res.status === 0) {
58
+ const ver = (res.stdout || res.stderr || Buffer.alloc(0)).toString().trim();
59
+ // Must be Python 3
60
+ if (/Python 3\./.test(ver)) return exe;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // ── venv python path (OS-aware) ───────────────────────────────────────────────
67
+
68
+ const venvPython = process.platform === 'win32'
69
+ ? path.join(venvDir, 'Scripts', 'python.exe')
70
+ : path.join(venvDir, 'bin', 'python');
71
+
72
+ // ── hash requirements.txt ────────────────────────────────────────────────────
73
+
74
+ function reqHash() {
75
+ if (!fs.existsSync(reqPath)) return null;
76
+ return crypto.createHash('sha256').update(fs.readFileSync(reqPath)).digest('hex');
77
+ }
78
+
79
+ function savedHash() {
80
+ if (!fs.existsSync(lockFile)) return null;
81
+ return fs.readFileSync(lockFile, 'utf8').trim();
82
+ }
83
+
84
+ // ── main ─────────────────────────────────────────────────────────────────────
85
+
86
+ banner('Checking Python environment for SAGE governance runtime…');
87
+
88
+ // Ensure requirements.txt exists
89
+ if (!fs.existsSync(reqPath)) {
90
+ warn('requirements.txt not found — skipping Python setup.');
91
+ process.exit(0);
92
+ }
93
+
94
+ // Check if re-install is needed
95
+ const currentHash = reqHash();
96
+ const prevHash = savedHash();
97
+ const venvReady = fs.existsSync(venvPython);
98
+
99
+ if (venvReady && currentHash === prevHash) {
100
+ success('Python environment is up to date. No action needed.');
101
+ process.exit(0);
102
+ }
103
+
104
+ // Locate Python 3 on the host
105
+ const python3 = findPython3();
106
+ if (!python3) {
107
+ fatal(
108
+ 'Python 3 not found in PATH.\n' +
109
+ ' Please install Python 3.9+ from https://python.org and re-run:\n' +
110
+ ' npm install -g sage-governance'
111
+ );
112
+ }
113
+
114
+ // Create or recreate venv
115
+ if (!venvReady) {
116
+ banner(`Creating virtual environment at ${venvDir} …`);
117
+ fs.mkdirSync(cacheDir, { recursive: true });
118
+ run(python3, ['-m', 'venv', venvDir]);
119
+ success('Virtual environment created.');
120
+ } else {
121
+ banner('requirements.txt has changed — refreshing dependencies…');
122
+ }
123
+
124
+ // Upgrade pip silently
125
+ banner('Upgrading pip…');
126
+ run(venvPython, ['-m', 'pip', 'install', '--upgrade', 'pip', '-q']);
127
+
128
+ // Install requirements
129
+ banner('Installing Python dependencies from requirements.txt…');
130
+ run(venvPython, ['-m', 'pip', 'install', '-r', reqPath]);
131
+
132
+ // Write lock file
133
+ fs.mkdirSync(cacheDir, { recursive: true });
134
+ fs.writeFileSync(lockFile, currentHash);
135
+
136
+ success('Python environment ready.');
137
+ banner(`Venv location : ${venvDir}`);
138
+ banner(`Lock written : ${lockFile}`);
package/security.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability within SAGE, please do not open a public issue. Instead, report it privately.
6
+
7
+ Please contact the maintainers directly at security@olustar.io.
8
+
9
+ We will acknowledge your report within 48 hours and provide a timeline for fixing the issue.
package/trae.json CHANGED
@@ -11,6 +11,29 @@
11
11
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
12
12
  "SAGE_LLM_MODEL": "gpt-4o-mini"
13
13
  }
14
+ },
15
+ "duckduckgo-search": {
16
+ "type": "local",
17
+ "command": ["uvx", "duckduckgo-mcp-server"],
18
+ "enabled": true,
19
+ "timeout": 10000
20
+ },
21
+ "github": {
22
+ "type": "local",
23
+ "command": [
24
+ "docker",
25
+ "run",
26
+ "-i",
27
+ "--rm",
28
+ "-e",
29
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
30
+ "ghcr.io/github/github-mcp-server"
31
+ ],
32
+ "enabled": false,
33
+ "environment": {
34
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
35
+ },
36
+ "timeout": 10000
14
37
  }
15
38
  }
16
39
  }
package/windsurf.json CHANGED
@@ -10,6 +10,29 @@
10
10
  "OPENAI_API_KEY": "${OPENAI_API_KEY}",
11
11
  "SAGE_LLM_MODEL": "gpt-4o-mini"
12
12
  }
13
+ },
14
+ "duckduckgo-search": {
15
+ "type": "local",
16
+ "command": ["uvx", "duckduckgo-mcp-server"],
17
+ "enabled": true,
18
+ "timeout": 10000
19
+ },
20
+ "github": {
21
+ "type": "local",
22
+ "command": [
23
+ "docker",
24
+ "run",
25
+ "-i",
26
+ "--rm",
27
+ "-e",
28
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
29
+ "ghcr.io/github/github-mcp-server"
30
+ ],
31
+ "enabled": false,
32
+ "environment": {
33
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PERSONAL_ACCESS_TOKEN}"
34
+ },
35
+ "timeout": 10000
13
36
  }
14
37
  }
15
38
  }