@jaguilar87/gaia-ops 5.0.0-beta.3 → 5.0.0-beta.4
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/gaia-doctor.js +7 -1
- package/bin/gaia-review.js +3 -1
- package/bin/gaia-scan +7 -1
- package/bin/gaia-skills-diagnose.js +4 -2
- package/bin/gaia-update.js +19 -14
- package/bin/pre-publish-validate.js +5 -2
- package/bin/python-detect.js +60 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +17 -1
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +2 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +26 -1
- package/dist/gaia-ops/tools/scan/verify.py +17 -13
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/modules/context/context_injector.py +17 -1
- package/hooks/modules/context/context_injector.py +17 -1
- package/package.json +1 -1
- package/skills/skill-creation/SKILL.md +2 -0
- package/tools/scan/scanners/environment.py +26 -1
- package/tools/scan/verify.py +17 -13
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "gaia-security",
|
|
10
10
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
11
|
-
"version": "5.0.0-beta.
|
|
11
|
+
"version": "5.0.0-beta.4",
|
|
12
12
|
"source": "./dist/gaia-security"
|
|
13
13
|
}
|
|
14
14
|
]
|
package/bin/gaia-doctor.js
CHANGED
|
@@ -18,6 +18,7 @@ import fs from 'fs/promises';
|
|
|
18
18
|
import { existsSync } from 'fs';
|
|
19
19
|
import { exec } from 'child_process';
|
|
20
20
|
import { promisify } from 'util';
|
|
21
|
+
import { findPython } from './python-detect.js';
|
|
21
22
|
import chalk from 'chalk';
|
|
22
23
|
import yargs from 'yargs';
|
|
23
24
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -200,8 +201,13 @@ async function checkProjectContext() {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
async function checkPython() {
|
|
204
|
+
const pyCmd = findPython();
|
|
205
|
+
if (!pyCmd) {
|
|
206
|
+
return { name: 'Python', ok: false, detail: 'Not found', fix: 'Install Python 3.9+' };
|
|
207
|
+
}
|
|
208
|
+
|
|
203
209
|
try {
|
|
204
|
-
const { stdout } = await execAsync(
|
|
210
|
+
const { stdout } = await execAsync(`${pyCmd} --version`);
|
|
205
211
|
const version = stdout.trim();
|
|
206
212
|
const match = version.match(/(\d+)\.(\d+)/);
|
|
207
213
|
|
package/bin/gaia-review.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { existsSync } from 'fs';
|
|
20
20
|
import { join } from 'path';
|
|
21
|
+
import { findPython } from './python-detect.js';
|
|
21
22
|
import chalk from 'chalk';
|
|
22
23
|
import yargs from 'yargs';
|
|
23
24
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -49,7 +50,8 @@ function callReviewEngine(action, opts = {}) {
|
|
|
49
50
|
process.exit(1);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
const pyCmd = findPython() || 'python3';
|
|
54
|
+
let cmd = `${pyCmd} "${enginePath}" ${action}`;
|
|
53
55
|
if (opts.updateId) cmd += ` --update-id "${opts.updateId}"`;
|
|
54
56
|
if (opts.contextPath) cmd += ` --context-path "${opts.contextPath}"`;
|
|
55
57
|
cmd += ' --json';
|
package/bin/gaia-scan
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { execFileSync } from 'child_process';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { dirname, join } from 'path';
|
|
15
|
+
import { requirePython } from './python-detect.js';
|
|
15
16
|
|
|
16
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
18
|
const __dirname = dirname(__filename);
|
|
@@ -28,11 +29,16 @@ const pythonPath = process.env.PYTHONPATH
|
|
|
28
29
|
: pluginRoot;
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
+
const pyCmd = requirePython();
|
|
33
|
+
execFileSync(pyCmd, [scanScript, ...args], {
|
|
32
34
|
stdio: 'inherit',
|
|
33
35
|
env: { ...process.env, PYTHONPATH: pythonPath },
|
|
34
36
|
});
|
|
35
37
|
} catch (error) {
|
|
38
|
+
if (error.message && error.message.includes('Python 3 not found')) {
|
|
39
|
+
console.error(error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
36
42
|
// execFileSync throws on non-zero exit; propagate the exit code
|
|
37
43
|
process.exit(error.status || 1);
|
|
38
44
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import fs from "fs";
|
|
20
20
|
import path from "path";
|
|
21
|
+
import { findPython } from "./python-detect.js";
|
|
21
22
|
import { spawnSync } from "child_process";
|
|
22
23
|
import { fileURLToPath } from "url";
|
|
23
24
|
import chalk from "chalk";
|
|
@@ -728,7 +729,8 @@ function runTestProbe(ctx, findings, checks) {
|
|
|
728
729
|
"Phase1SkillsInjection or load_skills",
|
|
729
730
|
];
|
|
730
731
|
|
|
731
|
-
const
|
|
732
|
+
const pyCmd = findPython() || "python3";
|
|
733
|
+
const res = spawnSync(pyCmd, args, {
|
|
732
734
|
cwd: ctx.packageRoot,
|
|
733
735
|
encoding: "utf-8",
|
|
734
736
|
});
|
|
@@ -741,7 +743,7 @@ function runTestProbe(ctx, findings, checks) {
|
|
|
741
743
|
code: "PYTEST_PROBE_EXEC_FAILED",
|
|
742
744
|
title: "Unable to execute pytest probe",
|
|
743
745
|
detail: res.error.message,
|
|
744
|
-
evidence:
|
|
746
|
+
evidence: `${pyCmd} ${args.join(" ")}`,
|
|
745
747
|
remediation: "Install pytest and Python dependencies to run the probe.",
|
|
746
748
|
});
|
|
747
749
|
checks.push({ name: "test-probe", ok: false, detail: "execution failed" });
|
package/bin/gaia-update.js
CHANGED
|
@@ -37,6 +37,7 @@ import { exec } from 'child_process';
|
|
|
37
37
|
import { promisify } from 'util';
|
|
38
38
|
import chalk from 'chalk';
|
|
39
39
|
import ora from 'ora';
|
|
40
|
+
import { findPython } from './python-detect.js';
|
|
40
41
|
|
|
41
42
|
const execAsync = promisify(exec);
|
|
42
43
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -126,8 +127,9 @@ async function updateLocalPermissions() {
|
|
|
126
127
|
let gaiaPerms;
|
|
127
128
|
try {
|
|
128
129
|
const setupPath = join(__dirname, '..', 'hooks', 'modules', 'core', 'plugin_setup.py');
|
|
130
|
+
const pythonCmd = findPython() || 'python3';
|
|
129
131
|
const { stdout } = await execAsync(
|
|
130
|
-
|
|
132
|
+
`${pythonCmd} -c "
|
|
131
133
|
import ast, json, re
|
|
132
134
|
|
|
133
135
|
source = open('${setupPath.replace(/'/g, "\\'")}').read()
|
|
@@ -453,13 +455,16 @@ async function runVerification() {
|
|
|
453
455
|
}
|
|
454
456
|
}
|
|
455
457
|
|
|
456
|
-
// 2. Python available
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
458
|
+
// 2. Python available (try python3 first, fall back to python on Windows)
|
|
459
|
+
{
|
|
460
|
+
const pyCmd = findPython();
|
|
461
|
+
if (pyCmd) {
|
|
462
|
+
const { stdout } = await execAsync(`${pyCmd} --version`, { timeout: 5000 });
|
|
463
|
+
checks.push({ name: 'python3', ok: true, detail: stdout.trim() });
|
|
464
|
+
} else {
|
|
465
|
+
checks.push({ name: 'python3', ok: false });
|
|
466
|
+
issues.push('Python 3 not found (required for hooks)');
|
|
467
|
+
}
|
|
463
468
|
}
|
|
464
469
|
|
|
465
470
|
// 3. project-context.json exists and is valid
|
|
@@ -540,12 +545,12 @@ async function runFreshInstall() {
|
|
|
540
545
|
|
|
541
546
|
console.log(chalk.cyan(`\n gaia-ops ${chalk.green(current)} — fresh install\n`));
|
|
542
547
|
|
|
543
|
-
// 1. Check Python 3 is available
|
|
548
|
+
// 1. Check Python 3 is available (try python3, then python)
|
|
544
549
|
const spinner = ora('Checking Python 3...').start();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
spinner.succeed(
|
|
548
|
-
}
|
|
550
|
+
const pyCmd = findPython();
|
|
551
|
+
if (pyCmd) {
|
|
552
|
+
spinner.succeed(`Python 3 found (${pyCmd})`);
|
|
553
|
+
} else {
|
|
549
554
|
spinner.warn('Python 3 not found — skipping project setup');
|
|
550
555
|
console.log(chalk.gray(' Install Python 3.9+ and run: npx gaia-scan\n'));
|
|
551
556
|
return;
|
|
@@ -555,7 +560,7 @@ async function runFreshInstall() {
|
|
|
555
560
|
const scanSpinner = ora('Running gaia-scan...').start();
|
|
556
561
|
try {
|
|
557
562
|
const { stdout, stderr } = await execAsync(
|
|
558
|
-
|
|
563
|
+
`${pyCmd} "${scanScript}" --npm-postinstall --root "${CWD}"`,
|
|
559
564
|
{ timeout: 60000 }
|
|
560
565
|
);
|
|
561
566
|
scanSpinner.succeed('Project scanned and configured');
|
|
@@ -21,6 +21,7 @@ import path from 'path';
|
|
|
21
21
|
import { execSync } from 'child_process';
|
|
22
22
|
import chalk from 'chalk';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
|
+
import { findPython } from './python-detect.js';
|
|
24
25
|
|
|
25
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
27
|
const __dirname = path.dirname(__filename);
|
|
@@ -371,15 +372,17 @@ class PrePublishValidator {
|
|
|
371
372
|
'tools/context/context_provider.py'
|
|
372
373
|
];
|
|
373
374
|
|
|
375
|
+
const pyCmd = findPython();
|
|
376
|
+
if (!pyCmd) throw new Error('Python not available');
|
|
374
377
|
pythonFiles.forEach(file => {
|
|
375
378
|
const filePath = path.join(baseDir, file);
|
|
376
379
|
if (fs.existsSync(filePath)) {
|
|
377
|
-
this.execute(
|
|
380
|
+
this.execute(`${pyCmd} -m py_compile "${filePath}"`, GAIA_OPS_ROOT, true);
|
|
378
381
|
this.log(` ✓ ${file}`, 'success');
|
|
379
382
|
}
|
|
380
383
|
});
|
|
381
384
|
} catch (error) {
|
|
382
|
-
this.log(` ⚠️ Python validation skipped (python3 not available or syntax error)`, 'warning');
|
|
385
|
+
this.log(` ⚠️ Python validation skipped (python3/python not available or syntax error)`, 'warning');
|
|
383
386
|
}
|
|
384
387
|
|
|
385
388
|
// Test 3: Check if bin scripts are executable
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform Python 3 detection.
|
|
3
|
+
*
|
|
4
|
+
* On Linux/macOS the binary is typically `python3`.
|
|
5
|
+
* On Windows it is often just `python` (the Microsoft Store alias
|
|
6
|
+
* or the official installer both register `python`).
|
|
7
|
+
*
|
|
8
|
+
* This module tries each candidate in order and returns the first
|
|
9
|
+
* one that reports Python 3.x. The result is cached for the
|
|
10
|
+
* lifetime of the process so repeated calls are free.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync, execFileSync } from 'child_process';
|
|
14
|
+
|
|
15
|
+
/** @type {string | null | undefined} */
|
|
16
|
+
let _cached;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect a working Python 3 command.
|
|
20
|
+
*
|
|
21
|
+
* @returns {string | null} The command name ('python3' or 'python'),
|
|
22
|
+
* or null if no Python 3 was found.
|
|
23
|
+
*/
|
|
24
|
+
export function findPython() {
|
|
25
|
+
if (_cached !== undefined) return _cached;
|
|
26
|
+
|
|
27
|
+
for (const cmd of ['python3', 'python']) {
|
|
28
|
+
try {
|
|
29
|
+
const version = execFileSync(cmd, ['--version'], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
}).trim();
|
|
34
|
+
if (version.startsWith('Python 3.')) {
|
|
35
|
+
_cached = cmd;
|
|
36
|
+
return _cached;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Not found or not Python 3 -- try next candidate
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_cached = null;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return the Python command or throw with a helpful message.
|
|
49
|
+
*
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function requirePython() {
|
|
53
|
+
const cmd = findPython();
|
|
54
|
+
if (!cmd) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Python 3 not found. Install Python 3.9+ and ensure "python3" or "python" is on PATH.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return cmd;
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-ops",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.4",
|
|
4
4
|
"description": "Full DevOps orchestration for Claude Code. Six specialized agents handle the complete development lifecycle \u2014 analysis, planning, execution, and deployment. Gaia-Ops scans your codebase to understand it and injects the right context into each sub-agent. Every command is classified by risk: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87"
|
|
@@ -10,7 +10,9 @@ Handles:
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
+
import shutil
|
|
13
14
|
import subprocess
|
|
15
|
+
import sys
|
|
14
16
|
from datetime import datetime
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
@@ -22,6 +24,20 @@ from .contracts_loader import build_context_update_reminder
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def _find_python() -> str:
|
|
28
|
+
"""Return the Python 3 command name for this platform.
|
|
29
|
+
|
|
30
|
+
Tries ``python3`` first (Linux/macOS), then ``python`` (Windows).
|
|
31
|
+
Falls back to ``sys.executable`` (the current interpreter) as a
|
|
32
|
+
last resort -- this always works since hooks are already running
|
|
33
|
+
under Python.
|
|
34
|
+
"""
|
|
35
|
+
for cmd in ("python3", "python"):
|
|
36
|
+
if shutil.which(cmd):
|
|
37
|
+
return cmd
|
|
38
|
+
return sys.executable
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
def _prune_empty_values(data: dict) -> dict:
|
|
26
42
|
"""Drop keys with empty telemetry values while preserving False/0."""
|
|
27
43
|
pruned = {}
|
|
@@ -288,7 +304,7 @@ def build_project_context(
|
|
|
288
304
|
# Execute context_provider.py to get filtered context
|
|
289
305
|
logger.info(f"Building context for {subagent_type}...")
|
|
290
306
|
result = subprocess.run(
|
|
291
|
-
[
|
|
307
|
+
[_find_python(), str(context_provider), subagent_type, prompt],
|
|
292
308
|
capture_output=True,
|
|
293
309
|
text=True,
|
|
294
310
|
timeout=15,
|
|
@@ -60,6 +60,8 @@ The test: for each rule or step you write, ask — if the agent saw enough real
|
|
|
60
60
|
|
|
61
61
|
This is why the investigation skill doesn't say "INVESTIGATE FIRST. ALWAYS. NO EXCEPTIONS." It says: *"Every codebase is a record of accumulated decisions... The first 2-3 files you read define whether your solution fits or fights the project."* The agent understands the stakes. The behavior follows.
|
|
62
62
|
|
|
63
|
+
Every line in a skill competes for weight in the LLM's reasoning. A rule without context carries almost no weight — the model has no reason to prioritize it over competing signals. An explanation with consequences carries enough weight to influence decisions even under pressure. This is why conciseness matters: a verbose skill dilutes its own weight. Every line should earn its place by adding reasoning the model can use.
|
|
64
|
+
|
|
63
65
|
### Tone by type
|
|
64
66
|
|
|
65
67
|
**Discipline** works best when the Iron Law is blunt and a reasoned paragraph follows explaining what breaks when you violate it. Command-execution's mental model ("When you reach for a pipe, you have not looked for the flag yet") does more work than a dozen capitalized warnings because it reframes the decision point itself.
|
|
@@ -35,6 +35,12 @@ _RUNTIME_DEFINITIONS: List[Tuple[str, str]] = [
|
|
|
35
35
|
("java", "--version"),
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
# On Windows, python3 may not exist -- fall back to "python" if it reports 3.x.
|
|
39
|
+
# Maps (fallback_binary, version_flag) -> canonical_name
|
|
40
|
+
_RUNTIME_FALLBACKS: Dict[str, List[Tuple[str, str]]] = {
|
|
41
|
+
"python3": [("python", "--version")],
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
# Env file names to check for (presence ONLY -- never read contents)
|
|
39
45
|
_ENV_FILE_NAMES: List[str] = [
|
|
40
46
|
".env",
|
|
@@ -182,14 +188,32 @@ class EnvironmentScanner(BaseScanner):
|
|
|
182
188
|
"""Detect installed language runtimes via --version commands.
|
|
183
189
|
|
|
184
190
|
Uses shutil.which() to find binaries, then subprocess with 2s timeout
|
|
185
|
-
to get version strings.
|
|
191
|
+
to get version strings. For runtimes with fallbacks (e.g. python3 ->
|
|
192
|
+
python on Windows), the fallback is tried when the primary is missing
|
|
193
|
+
and the result is reported under the canonical name.
|
|
186
194
|
"""
|
|
187
195
|
runtimes: List[Dict[str, str]] = []
|
|
196
|
+
detected_canonical: set = set()
|
|
188
197
|
|
|
189
198
|
for binary_name, version_flag in _RUNTIME_DEFINITIONS:
|
|
190
199
|
try:
|
|
191
200
|
binary_path = shutil.which(binary_name)
|
|
192
201
|
if binary_path is None:
|
|
202
|
+
# Try fallbacks (e.g. python -> python3 on Windows)
|
|
203
|
+
fallbacks = _RUNTIME_FALLBACKS.get(binary_name, [])
|
|
204
|
+
for fb_binary, fb_flag in fallbacks:
|
|
205
|
+
fb_path = shutil.which(fb_binary)
|
|
206
|
+
if fb_path is None:
|
|
207
|
+
continue
|
|
208
|
+
version = self._get_version(fb_binary, fb_flag, warnings)
|
|
209
|
+
if version is not None and version.startswith("3."):
|
|
210
|
+
runtimes.append({
|
|
211
|
+
"name": binary_name, # canonical name
|
|
212
|
+
"version": version,
|
|
213
|
+
"path": fb_path,
|
|
214
|
+
})
|
|
215
|
+
detected_canonical.add(binary_name)
|
|
216
|
+
break
|
|
193
217
|
continue
|
|
194
218
|
|
|
195
219
|
version = self._get_version(binary_name, version_flag, warnings)
|
|
@@ -199,6 +223,7 @@ class EnvironmentScanner(BaseScanner):
|
|
|
199
223
|
"version": version,
|
|
200
224
|
"path": binary_path,
|
|
201
225
|
})
|
|
226
|
+
detected_canonical.add(binary_name)
|
|
202
227
|
|
|
203
228
|
except Exception as exc:
|
|
204
229
|
warnings.append(f"Runtime detection failed for {binary_name}: {exc}")
|
|
@@ -159,23 +159,27 @@ def check_project_context(project_root: Path) -> CheckResult:
|
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def check_python() -> CheckResult:
|
|
162
|
-
"""Verify that python3 is available.
|
|
162
|
+
"""Verify that python3 (or python on Windows) is available.
|
|
163
|
+
|
|
164
|
+
Tries ``python3`` first, then ``python`` for Windows compatibility.
|
|
163
165
|
|
|
164
166
|
Returns:
|
|
165
167
|
CheckResult with Python version.
|
|
166
168
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
for cmd in ("python3", "python"):
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
[cmd, "--version"],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
timeout=5,
|
|
176
|
+
)
|
|
177
|
+
if result.returncode == 0:
|
|
178
|
+
version = result.stdout.strip()
|
|
179
|
+
if version.startswith("Python 3."):
|
|
180
|
+
return CheckResult(name="Python", ok=True, detail=version)
|
|
181
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
182
|
+
pass
|
|
179
183
|
|
|
180
184
|
return CheckResult(
|
|
181
185
|
name="Python",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-security",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.4",
|
|
4
4
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87"
|
|
@@ -10,7 +10,9 @@ Handles:
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
+
import shutil
|
|
13
14
|
import subprocess
|
|
15
|
+
import sys
|
|
14
16
|
from datetime import datetime
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
@@ -22,6 +24,20 @@ from .contracts_loader import build_context_update_reminder
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def _find_python() -> str:
|
|
28
|
+
"""Return the Python 3 command name for this platform.
|
|
29
|
+
|
|
30
|
+
Tries ``python3`` first (Linux/macOS), then ``python`` (Windows).
|
|
31
|
+
Falls back to ``sys.executable`` (the current interpreter) as a
|
|
32
|
+
last resort -- this always works since hooks are already running
|
|
33
|
+
under Python.
|
|
34
|
+
"""
|
|
35
|
+
for cmd in ("python3", "python"):
|
|
36
|
+
if shutil.which(cmd):
|
|
37
|
+
return cmd
|
|
38
|
+
return sys.executable
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
def _prune_empty_values(data: dict) -> dict:
|
|
26
42
|
"""Drop keys with empty telemetry values while preserving False/0."""
|
|
27
43
|
pruned = {}
|
|
@@ -288,7 +304,7 @@ def build_project_context(
|
|
|
288
304
|
# Execute context_provider.py to get filtered context
|
|
289
305
|
logger.info(f"Building context for {subagent_type}...")
|
|
290
306
|
result = subprocess.run(
|
|
291
|
-
[
|
|
307
|
+
[_find_python(), str(context_provider), subagent_type, prompt],
|
|
292
308
|
capture_output=True,
|
|
293
309
|
text=True,
|
|
294
310
|
timeout=15,
|
|
@@ -10,7 +10,9 @@ Handles:
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
+
import shutil
|
|
13
14
|
import subprocess
|
|
15
|
+
import sys
|
|
14
16
|
from datetime import datetime
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
@@ -22,6 +24,20 @@ from .contracts_loader import build_context_update_reminder
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def _find_python() -> str:
|
|
28
|
+
"""Return the Python 3 command name for this platform.
|
|
29
|
+
|
|
30
|
+
Tries ``python3`` first (Linux/macOS), then ``python`` (Windows).
|
|
31
|
+
Falls back to ``sys.executable`` (the current interpreter) as a
|
|
32
|
+
last resort -- this always works since hooks are already running
|
|
33
|
+
under Python.
|
|
34
|
+
"""
|
|
35
|
+
for cmd in ("python3", "python"):
|
|
36
|
+
if shutil.which(cmd):
|
|
37
|
+
return cmd
|
|
38
|
+
return sys.executable
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
def _prune_empty_values(data: dict) -> dict:
|
|
26
42
|
"""Drop keys with empty telemetry values while preserving False/0."""
|
|
27
43
|
pruned = {}
|
|
@@ -288,7 +304,7 @@ def build_project_context(
|
|
|
288
304
|
# Execute context_provider.py to get filtered context
|
|
289
305
|
logger.info(f"Building context for {subagent_type}...")
|
|
290
306
|
result = subprocess.run(
|
|
291
|
-
[
|
|
307
|
+
[_find_python(), str(context_provider), subagent_type, prompt],
|
|
292
308
|
capture_output=True,
|
|
293
309
|
text=True,
|
|
294
310
|
timeout=15,
|
package/package.json
CHANGED
|
@@ -60,6 +60,8 @@ The test: for each rule or step you write, ask — if the agent saw enough real
|
|
|
60
60
|
|
|
61
61
|
This is why the investigation skill doesn't say "INVESTIGATE FIRST. ALWAYS. NO EXCEPTIONS." It says: *"Every codebase is a record of accumulated decisions... The first 2-3 files you read define whether your solution fits or fights the project."* The agent understands the stakes. The behavior follows.
|
|
62
62
|
|
|
63
|
+
Every line in a skill competes for weight in the LLM's reasoning. A rule without context carries almost no weight — the model has no reason to prioritize it over competing signals. An explanation with consequences carries enough weight to influence decisions even under pressure. This is why conciseness matters: a verbose skill dilutes its own weight. Every line should earn its place by adding reasoning the model can use.
|
|
64
|
+
|
|
63
65
|
### Tone by type
|
|
64
66
|
|
|
65
67
|
**Discipline** works best when the Iron Law is blunt and a reasoned paragraph follows explaining what breaks when you violate it. Command-execution's mental model ("When you reach for a pipe, you have not looked for the flag yet") does more work than a dozen capitalized warnings because it reframes the decision point itself.
|
|
@@ -35,6 +35,12 @@ _RUNTIME_DEFINITIONS: List[Tuple[str, str]] = [
|
|
|
35
35
|
("java", "--version"),
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
# On Windows, python3 may not exist -- fall back to "python" if it reports 3.x.
|
|
39
|
+
# Maps (fallback_binary, version_flag) -> canonical_name
|
|
40
|
+
_RUNTIME_FALLBACKS: Dict[str, List[Tuple[str, str]]] = {
|
|
41
|
+
"python3": [("python", "--version")],
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
# Env file names to check for (presence ONLY -- never read contents)
|
|
39
45
|
_ENV_FILE_NAMES: List[str] = [
|
|
40
46
|
".env",
|
|
@@ -182,14 +188,32 @@ class EnvironmentScanner(BaseScanner):
|
|
|
182
188
|
"""Detect installed language runtimes via --version commands.
|
|
183
189
|
|
|
184
190
|
Uses shutil.which() to find binaries, then subprocess with 2s timeout
|
|
185
|
-
to get version strings.
|
|
191
|
+
to get version strings. For runtimes with fallbacks (e.g. python3 ->
|
|
192
|
+
python on Windows), the fallback is tried when the primary is missing
|
|
193
|
+
and the result is reported under the canonical name.
|
|
186
194
|
"""
|
|
187
195
|
runtimes: List[Dict[str, str]] = []
|
|
196
|
+
detected_canonical: set = set()
|
|
188
197
|
|
|
189
198
|
for binary_name, version_flag in _RUNTIME_DEFINITIONS:
|
|
190
199
|
try:
|
|
191
200
|
binary_path = shutil.which(binary_name)
|
|
192
201
|
if binary_path is None:
|
|
202
|
+
# Try fallbacks (e.g. python -> python3 on Windows)
|
|
203
|
+
fallbacks = _RUNTIME_FALLBACKS.get(binary_name, [])
|
|
204
|
+
for fb_binary, fb_flag in fallbacks:
|
|
205
|
+
fb_path = shutil.which(fb_binary)
|
|
206
|
+
if fb_path is None:
|
|
207
|
+
continue
|
|
208
|
+
version = self._get_version(fb_binary, fb_flag, warnings)
|
|
209
|
+
if version is not None and version.startswith("3."):
|
|
210
|
+
runtimes.append({
|
|
211
|
+
"name": binary_name, # canonical name
|
|
212
|
+
"version": version,
|
|
213
|
+
"path": fb_path,
|
|
214
|
+
})
|
|
215
|
+
detected_canonical.add(binary_name)
|
|
216
|
+
break
|
|
193
217
|
continue
|
|
194
218
|
|
|
195
219
|
version = self._get_version(binary_name, version_flag, warnings)
|
|
@@ -199,6 +223,7 @@ class EnvironmentScanner(BaseScanner):
|
|
|
199
223
|
"version": version,
|
|
200
224
|
"path": binary_path,
|
|
201
225
|
})
|
|
226
|
+
detected_canonical.add(binary_name)
|
|
202
227
|
|
|
203
228
|
except Exception as exc:
|
|
204
229
|
warnings.append(f"Runtime detection failed for {binary_name}: {exc}")
|
package/tools/scan/verify.py
CHANGED
|
@@ -159,23 +159,27 @@ def check_project_context(project_root: Path) -> CheckResult:
|
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def check_python() -> CheckResult:
|
|
162
|
-
"""Verify that python3 is available.
|
|
162
|
+
"""Verify that python3 (or python on Windows) is available.
|
|
163
|
+
|
|
164
|
+
Tries ``python3`` first, then ``python`` for Windows compatibility.
|
|
163
165
|
|
|
164
166
|
Returns:
|
|
165
167
|
CheckResult with Python version.
|
|
166
168
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
for cmd in ("python3", "python"):
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
[cmd, "--version"],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
timeout=5,
|
|
176
|
+
)
|
|
177
|
+
if result.returncode == 0:
|
|
178
|
+
version = result.stdout.strip()
|
|
179
|
+
if version.startswith("Python 3."):
|
|
180
|
+
return CheckResult(name="Python", ok=True, detail=version)
|
|
181
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
182
|
+
pass
|
|
179
183
|
|
|
180
184
|
return CheckResult(
|
|
181
185
|
name="Python",
|