@asobacloud/nehanda 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/LICENSE.md +57 -0
- package/README.md +444 -2
- package/bin/agent.mjs +1260 -0
- package/bin/nehanda-ui.mjs +824 -0
- package/lib/anthropicHeaders.mjs +8 -0
- package/lib/app.mjs +313 -0
- package/lib/auth.mjs +255 -0
- package/lib/bashguard.mjs +159 -0
- package/lib/compact.mjs +306 -0
- package/lib/energy-middleware.mjs +318 -0
- package/lib/hookShapeOrder.mjs +30 -0
- package/lib/hookplane.mjs +228 -0
- package/lib/instructions.mjs +23 -0
- package/lib/jev_read.mjs +216 -0
- package/lib/jev_write.mjs +253 -0
- package/lib/laya.mjs +296 -0
- package/lib/laya_sidecar.mjs +198 -0
- package/lib/mcp-config.mjs +189 -0
- package/lib/mcp.mjs +169 -0
- package/lib/modelConfig.mjs +152 -0
- package/lib/modelDiscovery.mjs +205 -0
- package/lib/openaiCompat.mjs +147 -0
- package/lib/orchestrate.mjs +1163 -0
- package/lib/paths.mjs +20 -0
- package/lib/permissions.mjs +44 -0
- package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
- package/lib/scripts/audit-code-integrity.py +437 -0
- package/lib/scripts/ensure-laya-env.sh +55 -0
- package/lib/scripts/js-safety-checker.cjs +308 -0
- package/lib/scripts/laya_worker.py +171 -0
- package/lib/scripts/odse-transform.py +97 -0
- package/lib/scripts/python-safety-checker.py +197 -0
- package/lib/scripts/shell-safety-checker.sh +306 -0
- package/lib/settings.mjs +147 -0
- package/lib/store.mjs +278 -0
- package/lib/team.mjs +69 -0
- package/lib/testgen.mjs +146 -0
- package/lib/tools/AuditCodeIntegrity.json +30 -0
- package/lib/tools/JsSafetyChecker.json +29 -0
- package/lib/tools/PythonSafetyChecker.json +29 -0
- package/lib/tools/ShellSafetyChecker.json +29 -0
- package/lib/tools.mjs +907 -0
- package/lib/transcript.mjs +223 -0
- package/lib/ui.mjs +286 -0
- package/lib/workflow.mjs +91 -0
- package/lib/worktree.mjs +56 -0
- package/package.json +50 -4
- package/schema.sql +181 -0
- package/scripts/postinstall.mjs +62 -0
- package/scripts/sdlc-acceptance.sh +477 -0
- package/scripts/verify-sdlc-hook-order.mjs +26 -0
- package/templates/test_e2e.sh +32 -0
- package/templates/test_file.sh +14 -0
- package/templates/test_hook.sh +27 -0
- package/templates/test_http.sh +17 -0
- package/templates/test_phase.sh +23 -0
- package/templates/test_process.sh +14 -0
- package/templates/test_tool.sh +24 -0
- package/templates/validate_test.sh +55 -0
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export function onaHome() {
|
|
6
|
+
const h = process.env.ONAHOME || path.join(os.homedir(), '.ona')
|
|
7
|
+
fs.mkdirSync(h, { recursive: true, mode: 0o700 })
|
|
8
|
+
return h
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function defaultDbPath() {
|
|
12
|
+
return path.join(onaHome(), 'agent.db')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** §2.8 — secrets outside AGENT_SDLC_DB */
|
|
16
|
+
export function secureAuthPath() {
|
|
17
|
+
const d = path.join(onaHome(), 'secure')
|
|
18
|
+
fs.mkdirSync(d, { recursive: true, mode: 0o700 })
|
|
19
|
+
return path.join(d, 'anthropic.json')
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** §5.12 — permission evaluation (deny > ask > allow > defaultMode). */
|
|
2
|
+
export function evaluatePermission(permissions, toolName, _toolInput, phase) {
|
|
3
|
+
const p = permissions || { defaultMode: 'default' }
|
|
4
|
+
const defaultMode = p.defaultMode || 'default'
|
|
5
|
+
|
|
6
|
+
if (matchesAny(p.deny, toolName)) return 'deny'
|
|
7
|
+
if (matchesAny(p.ask, toolName)) return 'ask'
|
|
8
|
+
if (matchesAny(p.allow, toolName)) return 'allow'
|
|
9
|
+
|
|
10
|
+
switch (defaultMode) {
|
|
11
|
+
case 'bypassPermissions': return 'allow'
|
|
12
|
+
case 'dontAsk': return 'deny'
|
|
13
|
+
case 'acceptEdits':
|
|
14
|
+
return (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') ? 'allow' : 'ask'
|
|
15
|
+
case 'plan':
|
|
16
|
+
return (toolName === 'Write' || toolName === 'Edit' || toolName === 'Bash' || toolName === 'NotebookEdit') ? 'deny' : 'ask'
|
|
17
|
+
case 'default':
|
|
18
|
+
default: {
|
|
19
|
+
// In implement/test phases the user already approved the plan — auto-allow all execution tools
|
|
20
|
+
if (phase === 'implement' || phase === 'test') return 'allow'
|
|
21
|
+
|
|
22
|
+
// Read-only/safe tools auto-allowed in all other modes
|
|
23
|
+
const AUTO_ALLOW = new Set(['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch',
|
|
24
|
+
'ToolSearch', 'ListMcpResources', 'ReadMcpResource',
|
|
25
|
+
'AskUserQuestion', 'Brief', 'TodoWrite', 'TaskOutput', 'TaskStop', 'Skill'])
|
|
26
|
+
return AUTO_ALLOW.has(toolName) ? 'allow' : 'ask'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchesAny(rules, toolName) {
|
|
32
|
+
if (!Array.isArray(rules)) return false
|
|
33
|
+
for (const r of rules) {
|
|
34
|
+
if (typeof r !== 'string' || !r.trim()) continue
|
|
35
|
+
if (ruleMatches(r.trim(), toolName)) return true
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ruleMatches(rule, toolName) {
|
|
41
|
+
if (rule === toolName) return true
|
|
42
|
+
if (rule.endsWith('*')) return toolName.startsWith(rule.slice(0, -1))
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
audit-code-integrity.py — Ona Platform Manual Code Integrity Auditor
|
|
4
|
+
|
|
5
|
+
Audits repository files for:
|
|
6
|
+
1. Lifecycle Symmetry & Teardown Parity (Shell/Python resources)
|
|
7
|
+
2. Behavioral Test Authenticity (Mock Theater Detection)
|
|
8
|
+
3. Architectural Naming & Contract Alignment
|
|
9
|
+
4. Error Handling & Hygiene (Swallowed Exceptions & Root Clutter)
|
|
10
|
+
5. DRY Invariants & Duplicative Functions
|
|
11
|
+
|
|
12
|
+
Outputs formatted terminal reports and saves structured JSON logs to .review/<M-D-YYYY-XXX>-audit-report.json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import ast
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
WORKSPACE_ROOT = os.getcwd()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CodeIntegrityAuditor:
|
|
27
|
+
def __init__(self, target_path=None, verbose=False, output_dir=None):
|
|
28
|
+
self.target_path = os.path.abspath(target_path or WORKSPACE_ROOT)
|
|
29
|
+
self.verbose = verbose
|
|
30
|
+
base_dir = os.path.dirname(self.target_path) if os.path.isfile(self.target_path) else self.target_path
|
|
31
|
+
self.output_dir = output_dir or os.path.join(base_dir, ".review")
|
|
32
|
+
self.findings = {
|
|
33
|
+
"lifecycle_parity": [],
|
|
34
|
+
"behavioral_tests": [],
|
|
35
|
+
"architectural_naming": [],
|
|
36
|
+
"error_handling": [],
|
|
37
|
+
"dry_violations": [],
|
|
38
|
+
}
|
|
39
|
+
self.authenticity_stats = {
|
|
40
|
+
"tier_1_live": 0,
|
|
41
|
+
"tier_2_contract": 0,
|
|
42
|
+
"tier_3_theater": 0,
|
|
43
|
+
"total_test_functions": 0,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def run(self):
|
|
47
|
+
if os.path.isfile(self.target_path):
|
|
48
|
+
files_to_scan = [self.target_path]
|
|
49
|
+
base_dir = os.path.dirname(self.target_path)
|
|
50
|
+
else:
|
|
51
|
+
files_to_scan = self._get_files_to_scan(self.target_path)
|
|
52
|
+
base_dir = self.target_path
|
|
53
|
+
|
|
54
|
+
for file_path in files_to_scan:
|
|
55
|
+
rel_path = os.path.relpath(file_path, base_dir) if file_path.startswith(base_dir) else file_path
|
|
56
|
+
if file_path.endswith(".sh"):
|
|
57
|
+
self._audit_shell_file(file_path, rel_path)
|
|
58
|
+
elif file_path.endswith(".py"):
|
|
59
|
+
self._audit_python_file(file_path, rel_path)
|
|
60
|
+
|
|
61
|
+
self._audit_duplicative_functions(files_to_scan, base_dir)
|
|
62
|
+
|
|
63
|
+
report_path = self._save_json_report()
|
|
64
|
+
return report_path
|
|
65
|
+
|
|
66
|
+
def _get_files_to_scan(self, root_dir):
|
|
67
|
+
files = []
|
|
68
|
+
skip_dirs = {"node_modules", "venv", ".venv", "/tmp/"}
|
|
69
|
+
for dirpath, dirnames, filenames in os.walk(root_dir):
|
|
70
|
+
dirnames[:] = [d for d in dirnames if d not in skip_dirs and not d.startswith(".")]
|
|
71
|
+
for f in filenames:
|
|
72
|
+
if f.endswith(".sh") or f.endswith(".py"):
|
|
73
|
+
files.append(os.path.join(dirpath, f))
|
|
74
|
+
return sorted(files)
|
|
75
|
+
|
|
76
|
+
def _audit_shell_file(self, file_path, rel_path):
|
|
77
|
+
try:
|
|
78
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
79
|
+
content = f.read()
|
|
80
|
+
lines = content.splitlines()
|
|
81
|
+
except Exception:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
has_trap = "trap " in content or "cleanup()" in content
|
|
85
|
+
|
|
86
|
+
# 1. Resource Allocations vs Teardown Parity
|
|
87
|
+
creations = [
|
|
88
|
+
("docker buildx create", "docker buildx rm", "buildx builder creation"),
|
|
89
|
+
("docker context create", "docker context rm", "docker context creation"),
|
|
90
|
+
("aws ec2 run-instances", "aws ec2 terminate-instances", "EC2 instance launch"),
|
|
91
|
+
("aws ec2 authorize-security-group-ingress", "aws ec2 revoke-security-group-ingress", "security group ingress rule"),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for create_cmd, remove_cmd, resource_desc in creations:
|
|
95
|
+
if create_cmd in content:
|
|
96
|
+
if not has_trap or remove_cmd not in content:
|
|
97
|
+
self.findings["lifecycle_parity"].append({
|
|
98
|
+
"file": rel_path,
|
|
99
|
+
"severity": "FAIL",
|
|
100
|
+
"rule": "LIFECYCLE_ASYMMETRY",
|
|
101
|
+
"message": f"Script contains '{create_cmd}' ({resource_desc}) but lacks matching '{remove_cmd}' in cleanup trap.",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# 2. Forced Teardown Checks (--force or -f)
|
|
105
|
+
for idx, line in enumerate(lines, 1):
|
|
106
|
+
if "docker buildx rm" in line and "--force" not in line and "-f" not in line:
|
|
107
|
+
self.findings["lifecycle_parity"].append({
|
|
108
|
+
"file": rel_path,
|
|
109
|
+
"line": idx,
|
|
110
|
+
"severity": "FAIL",
|
|
111
|
+
"rule": "BLOCKING_TEARDOWN",
|
|
112
|
+
"message": "Call to 'docker buildx rm' lacks '--force' / '-f' flag, which can hang on dead SSH hosts.",
|
|
113
|
+
})
|
|
114
|
+
if "docker context rm" in line and "2>/dev/null" not in line and "|| true" not in line:
|
|
115
|
+
self.findings["lifecycle_parity"].append({
|
|
116
|
+
"file": rel_path,
|
|
117
|
+
"line": idx,
|
|
118
|
+
"severity": "WARN",
|
|
119
|
+
"rule": "UNGUARDED_CONTEXT_RM",
|
|
120
|
+
"message": "Call to 'docker context rm' lacks error guard ('2>/dev/null || true').",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# 3. Global Ambient State Leak Check
|
|
124
|
+
if "docker buildx use" in content and "docker buildx use default" not in content:
|
|
125
|
+
self.findings["lifecycle_parity"].append({
|
|
126
|
+
"file": rel_path,
|
|
127
|
+
"severity": "WARN",
|
|
128
|
+
"rule": "AMBIENT_STATE_LEAK",
|
|
129
|
+
"message": "Script modifies active buildx builder ('docker buildx use') without restoring default builder in cleanup.",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
def _audit_python_file(self, file_path, rel_path):
|
|
133
|
+
try:
|
|
134
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
135
|
+
content = f.read()
|
|
136
|
+
tree = ast.parse(content, filename=file_path)
|
|
137
|
+
except Exception:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
is_test_file = (
|
|
141
|
+
"test_" in rel_path
|
|
142
|
+
or "_test" in rel_path
|
|
143
|
+
or "tests/" in rel_path
|
|
144
|
+
or re.search(r"^\s*def test_", content, re.MULTILINE) is not None
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Scan AST nodes
|
|
148
|
+
for node in ast.walk(tree):
|
|
149
|
+
# 1. Behavioral Test Authenticity (Mock Theater Detection)
|
|
150
|
+
if is_test_file and isinstance(node, ast.FunctionDef) and node.name.startswith("test_"):
|
|
151
|
+
self._check_mock_theater(node, rel_path, content)
|
|
152
|
+
|
|
153
|
+
# 2. Architectural Naming Verification
|
|
154
|
+
if isinstance(node, ast.Assign):
|
|
155
|
+
for target in node.targets:
|
|
156
|
+
if isinstance(target, ast.Name) and "LAMBDA" in target.id.upper():
|
|
157
|
+
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
158
|
+
val = node.value.value
|
|
159
|
+
if val and not val.startswith("ona-") and not val.startswith("/ona/") and not val.startswith("/aws/lambda/"):
|
|
160
|
+
self.findings["architectural_naming"].append({
|
|
161
|
+
"file": rel_path,
|
|
162
|
+
"line": node.lineno,
|
|
163
|
+
"severity": "FAIL",
|
|
164
|
+
"rule": "INVALID_LAMBDA_NAME",
|
|
165
|
+
"message": f"Lambda function variable '{target.id}' value '{val}' violates 'ona-{{service}}-{{stage}}' naming invariant.",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# 3. Swallowed Error Exceptions
|
|
169
|
+
if isinstance(node, ast.ExceptHandler):
|
|
170
|
+
if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):
|
|
171
|
+
self.findings["error_handling"].append({
|
|
172
|
+
"file": rel_path,
|
|
173
|
+
"line": node.lineno,
|
|
174
|
+
"severity": "WARN",
|
|
175
|
+
"rule": "SWALLOWED_EXCEPTION",
|
|
176
|
+
"message": "Exception handler silently swallows errors with bare 'pass' without logging.",
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
def _check_mock_theater(self, func_node, rel_path, content):
|
|
180
|
+
self.authenticity_stats["total_test_functions"] += 1
|
|
181
|
+
func_name = func_node.name
|
|
182
|
+
target_stem = func_name.replace("test_", "")
|
|
183
|
+
|
|
184
|
+
allowed_external_prefixes = (
|
|
185
|
+
"boto3", "botocore", "requests", "urllib", "sys", "os", "subprocess",
|
|
186
|
+
"pgvector", "pymysql", "redis", "psycopg2", "aiohttp", "fitz", "pdfplumber",
|
|
187
|
+
"builtins", "datetime", "time", "json", "math", "numpy", "pandas", "sklearn", "scipy"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
patched_targets = []
|
|
191
|
+
has_substantive_assert = False
|
|
192
|
+
has_any_assert = False
|
|
193
|
+
is_internal_mocked = False
|
|
194
|
+
is_live_aws = False
|
|
195
|
+
|
|
196
|
+
func_body_str = ast.unparse(func_node) if hasattr(ast, "unparse") else ""
|
|
197
|
+
if any(term in func_body_str for term in ["boto3.client", "boto3.resource", "requests.get", "requests.post", "urllib.request", "aws lambda", "aws e2e"]):
|
|
198
|
+
is_live_aws = True
|
|
199
|
+
|
|
200
|
+
# Walk nodes inside test function definition
|
|
201
|
+
for subnode in ast.walk(func_node):
|
|
202
|
+
# Inspect @patch(...) or with patch(...) or mocker.patch(...)
|
|
203
|
+
if isinstance(subnode, ast.Call):
|
|
204
|
+
func_str = ast.unparse(subnode.func) if hasattr(ast, "unparse") else ""
|
|
205
|
+
if "patch" in func_str and subnode.args:
|
|
206
|
+
arg0 = subnode.args[0]
|
|
207
|
+
if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
|
|
208
|
+
target_str = arg0.value
|
|
209
|
+
patched_targets.append(target_str)
|
|
210
|
+
|
|
211
|
+
# Check if internal service function is mocked
|
|
212
|
+
if ("services." in target_str or "app." in target_str or target_str.startswith("app")) and not any(target_str.startswith(p) for p in allowed_external_prefixes):
|
|
213
|
+
is_internal_mocked = True
|
|
214
|
+
self.findings["behavioral_tests"].append({
|
|
215
|
+
"file": rel_path,
|
|
216
|
+
"line": subnode.lineno,
|
|
217
|
+
"severity": "WARN",
|
|
218
|
+
"rule": "INTERNAL_FUNCTION_MOCKED",
|
|
219
|
+
"message": f"Test '{func_name}' patches internal function/module '{target_str}'. Behavioral tests must call internal service logic directly and only mock external boundaries.",
|
|
220
|
+
})
|
|
221
|
+
elif target_stem and (target_stem in target_str or target_str.endswith(f".{target_stem}")):
|
|
222
|
+
is_internal_mocked = True
|
|
223
|
+
self.findings["behavioral_tests"].append({
|
|
224
|
+
"file": rel_path,
|
|
225
|
+
"line": subnode.lineno,
|
|
226
|
+
"severity": "WARN",
|
|
227
|
+
"rule": "MOCK_THEATER_TARGET_MOCKED",
|
|
228
|
+
"message": f"Test '{func_name}' mocks the primary function under test ('{target_stem}'). Behavioral tests must call the actual target function directly.",
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
# Inspect Assert statements
|
|
232
|
+
if isinstance(subnode, ast.Assert):
|
|
233
|
+
has_any_assert = True
|
|
234
|
+
assert_str = ast.unparse(subnode.test) if hasattr(ast, "unparse") else ""
|
|
235
|
+
# Substantive assertions check equality, subscripting, in-comparisons, or value bounds
|
|
236
|
+
if any(op in assert_str for op in ["==", "!=", " in ", ">", "<", "[", "]"]):
|
|
237
|
+
has_substantive_assert = True
|
|
238
|
+
|
|
239
|
+
# Inspect mock assertion calls like mock.assert_called_once()
|
|
240
|
+
if isinstance(subnode, ast.Expr) and isinstance(subnode.value, ast.Call):
|
|
241
|
+
call_str = ast.unparse(subnode.value.func) if hasattr(ast, "unparse") else ""
|
|
242
|
+
if "assert_called" in call_str:
|
|
243
|
+
has_any_assert = True
|
|
244
|
+
|
|
245
|
+
# Flag tests with zero substantive outcome assertions (Assertion Theater)
|
|
246
|
+
if not has_substantive_assert:
|
|
247
|
+
self.findings["behavioral_tests"].append({
|
|
248
|
+
"file": rel_path,
|
|
249
|
+
"line": func_node.lineno,
|
|
250
|
+
"severity": "WARN",
|
|
251
|
+
"rule": "ASSERTION_THEATER",
|
|
252
|
+
"message": f"Test '{func_name}' has no substantive outcome value assertions (e.g. data contract keys, non-zero ranges, schema checks). Testing mocks without outcome checks is Mock Theater.",
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
# Classify Tier
|
|
256
|
+
if is_live_aws:
|
|
257
|
+
self.authenticity_stats["tier_1_live"] += 1
|
|
258
|
+
elif has_substantive_assert and not is_internal_mocked:
|
|
259
|
+
self.authenticity_stats["tier_2_contract"] += 1
|
|
260
|
+
else:
|
|
261
|
+
self.authenticity_stats["tier_3_theater"] += 1
|
|
262
|
+
|
|
263
|
+
def _audit_duplicative_functions(self, files_to_scan, base_dir):
|
|
264
|
+
ignored_names = {
|
|
265
|
+
"main", "handler", "lambda_handler", "cleanup", "__init__", "setUp", "tearDown",
|
|
266
|
+
"run", "execute", "cleanup_on_failure", "require_cmd", "app"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func_names_seen = {}
|
|
270
|
+
ast_hashes_seen = {}
|
|
271
|
+
|
|
272
|
+
for file_path in files_to_scan:
|
|
273
|
+
if not file_path.endswith(".py"):
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
rel_path = os.path.relpath(file_path, base_dir) if file_path.startswith(base_dir) else file_path
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
280
|
+
content = f.read()
|
|
281
|
+
tree = ast.parse(content)
|
|
282
|
+
except Exception:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
for node in ast.walk(tree):
|
|
286
|
+
if isinstance(node, ast.FunctionDef):
|
|
287
|
+
fname = node.name
|
|
288
|
+
if fname.startswith("_") or fname in ignored_names or fname.startswith("test_"):
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# 1. Duplicate Function Name Check
|
|
292
|
+
if fname in func_names_seen:
|
|
293
|
+
first_rel, first_line = func_names_seen[fname]
|
|
294
|
+
if first_rel != rel_path:
|
|
295
|
+
self.findings["dry_violations"].append({
|
|
296
|
+
"file": rel_path,
|
|
297
|
+
"line": node.lineno,
|
|
298
|
+
"severity": "WARN",
|
|
299
|
+
"rule": "DUPLICATE_FUNCTION_NAME",
|
|
300
|
+
"message": f"Function '{fname}' is duplicated across files (first defined in '{first_rel}:L{first_line}'). Share logic via central package import instead of repeating function definitions.",
|
|
301
|
+
})
|
|
302
|
+
else:
|
|
303
|
+
func_names_seen[fname] = (rel_path, node.lineno)
|
|
304
|
+
|
|
305
|
+
# 2. AST Structural Code Clone Check (>= 2 statements)
|
|
306
|
+
if len(node.body) >= 2:
|
|
307
|
+
try:
|
|
308
|
+
node_copy = ast.parse(ast.unparse(node)) if hasattr(ast, "unparse") else node
|
|
309
|
+
ast_str = ast.dump(node_copy, annotate_fields=False, include_attributes=False)
|
|
310
|
+
norm_ast = re.sub(r"'[a-zA-Z0-9_]+'", "'VAR'", ast_str)
|
|
311
|
+
|
|
312
|
+
if norm_ast in ast_hashes_seen:
|
|
313
|
+
first_name, first_rel, first_line = ast_hashes_seen[norm_ast]
|
|
314
|
+
if first_rel != rel_path or first_name != fname:
|
|
315
|
+
self.findings["dry_violations"].append({
|
|
316
|
+
"file": rel_path,
|
|
317
|
+
"line": node.lineno,
|
|
318
|
+
"severity": "WARN",
|
|
319
|
+
"rule": "DUPLICATE_FUNCTION_LOGIC",
|
|
320
|
+
"message": f"Function '{fname}' has identical AST code logic structure as '{first_name}' in '{first_rel}:L{first_line}'. Refactor into shared module.",
|
|
321
|
+
})
|
|
322
|
+
else:
|
|
323
|
+
ast_hashes_seen[norm_ast] = (fname, rel_path, node.lineno)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
def _save_json_report(self):
|
|
328
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
329
|
+
now = datetime.datetime.now()
|
|
330
|
+
date_str = f"{now.month}-{now.day}-{now.year}"
|
|
331
|
+
|
|
332
|
+
# Calculate next sequence number for today
|
|
333
|
+
existing = [f for f in os.listdir(self.output_dir) if f.startswith(date_str) and f.endswith("-audit-report.json")]
|
|
334
|
+
seq = len(existing) + 1
|
|
335
|
+
filename = f"{date_str}-{seq:03d}-audit-report.json"
|
|
336
|
+
report_file = os.path.join(self.output_dir, filename)
|
|
337
|
+
|
|
338
|
+
total_fails = sum(1 for cat in self.findings.values() for item in cat if item.get("severity") == "FAIL")
|
|
339
|
+
total_warns = sum(1 for cat in self.findings.values() for item in cat if item.get("severity") == "WARN")
|
|
340
|
+
|
|
341
|
+
payload = {
|
|
342
|
+
"timestamp": now.isoformat(),
|
|
343
|
+
"target": self.target_path,
|
|
344
|
+
"summary": {
|
|
345
|
+
"total_failures": total_fails,
|
|
346
|
+
"total_warnings": total_warns,
|
|
347
|
+
"status": "FAIL" if total_fails > 0 else "PASS",
|
|
348
|
+
"authenticity_stats": self.authenticity_stats,
|
|
349
|
+
},
|
|
350
|
+
"findings": self.findings,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
with open(report_file, "w", encoding="utf-8") as f:
|
|
354
|
+
json.dump(payload, f, indent=2)
|
|
355
|
+
|
|
356
|
+
return report_file
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def print_formatted_summary(auditor, report_path):
|
|
360
|
+
print("=" * 80)
|
|
361
|
+
print(" ONA PLATFORM CODE INTEGRITY AUDIT REPORT ")
|
|
362
|
+
print("=" * 80)
|
|
363
|
+
|
|
364
|
+
# Print Authenticity Breakdown Table
|
|
365
|
+
stats = auditor.authenticity_stats
|
|
366
|
+
total_tests = stats["total_test_functions"]
|
|
367
|
+
if total_tests > 0:
|
|
368
|
+
t1_pct = (stats["tier_1_live"] / total_tests) * 100
|
|
369
|
+
t2_pct = (stats["tier_2_contract"] / total_tests) * 100
|
|
370
|
+
t3_pct = (stats["tier_3_theater"] / total_tests) * 100
|
|
371
|
+
print("\nTEST SUITE AUTHENTICITY CLASSIFICATION:")
|
|
372
|
+
print(f" Tier 1 (Live AWS / E2E Integration) : {stats['tier_1_live']:3d} tests ({t1_pct:5.1f}%) [GOLD STANDARD]")
|
|
373
|
+
print(f" Tier 2 (Contract-Enforced Behavioral): {stats['tier_2_contract']:3d} tests ({t2_pct:5.1f}%) [VALID LOGIC]")
|
|
374
|
+
print(f" Tier 3 (Mock Theater / Superficial) : {stats['tier_3_theater']:3d} tests ({t3_pct:5.1f}%) [DECEPTIVE THEATER]")
|
|
375
|
+
print("-" * 80)
|
|
376
|
+
|
|
377
|
+
total_fails = 0
|
|
378
|
+
total_warns = 0
|
|
379
|
+
|
|
380
|
+
category_titles = {
|
|
381
|
+
"lifecycle_parity": "Lifecycle Parity & Resource Teardown",
|
|
382
|
+
"behavioral_tests": "Behavioral Test Authenticity (Anti-Mock Theater)",
|
|
383
|
+
"architectural_naming": "Architectural Naming & Contract Invariants",
|
|
384
|
+
"error_handling": "Error Handling & Exception Hygiene",
|
|
385
|
+
"dry_violations": "DRY Invariants & Duplicative Functions",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for cat_key, items in auditor.findings.items():
|
|
389
|
+
title = category_titles.get(cat_key, cat_key)
|
|
390
|
+
fails = [i for i in items if i.get("severity") == "FAIL"]
|
|
391
|
+
warns = [i for i in items if i.get("severity") == "WARN"]
|
|
392
|
+
total_fails += len(fails)
|
|
393
|
+
total_warns += len(warns)
|
|
394
|
+
|
|
395
|
+
if not items:
|
|
396
|
+
print(f"[PASS] {title}")
|
|
397
|
+
else:
|
|
398
|
+
status_badge = "[FAIL]" if fails else "[WARN]"
|
|
399
|
+
print(f"{status_badge} {title} ({len(fails)} Failures, {len(warns)} Warnings)")
|
|
400
|
+
for item in items:
|
|
401
|
+
line_info = f":L{item['line']}" if "line" in item else ""
|
|
402
|
+
sev = f"[{item['severity']}]"
|
|
403
|
+
print(f" • {item['file']}{line_info} {sev} {item['message']}")
|
|
404
|
+
print()
|
|
405
|
+
|
|
406
|
+
print("=" * 80)
|
|
407
|
+
summary_status = "FAIL" if total_fails > 0 else "PASS"
|
|
408
|
+
print(f"OVERALL STATUS: {summary_status} ({total_fails} Failures, {total_warns} Warnings)")
|
|
409
|
+
print(f"Detailed audit log saved to: {report_path}")
|
|
410
|
+
print("=" * 80)
|
|
411
|
+
return total_fails
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def main():
|
|
415
|
+
parser = argparse.ArgumentParser(description="Ona Platform Manual Code Integrity Auditor")
|
|
416
|
+
parser.add_argument("--target", default=None, help="Target file or directory to scan (defaults to repository root)")
|
|
417
|
+
parser.add_argument("--verbose", action="store_true", help="Print verbose output")
|
|
418
|
+
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
|
|
419
|
+
parser.add_argument("--output-dir", default=None, help="Directory to save JSON report (defaults to .review/)")
|
|
420
|
+
|
|
421
|
+
args = parser.parse_args()
|
|
422
|
+
|
|
423
|
+
auditor = CodeIntegrityAuditor(target_path=args.target, verbose=args.verbose, output_dir=args.output_dir)
|
|
424
|
+
report_path = auditor.run()
|
|
425
|
+
|
|
426
|
+
if args.format == "json":
|
|
427
|
+
with open(report_path, "r") as f:
|
|
428
|
+
print(f.read())
|
|
429
|
+
total_fails = sum(1 for cat in auditor.findings.values() for item in cat if item.get("severity") == "FAIL")
|
|
430
|
+
sys.exit(1 if total_fails > 0 else 0)
|
|
431
|
+
|
|
432
|
+
fails = print_formatted_summary(auditor, report_path)
|
|
433
|
+
sys.exit(1 if fails > 0 else 0)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ensure-laya-env.sh — REQUIRED provisioning for System-1 (Laya).
|
|
3
|
+
# Creates ~/.nehanda/venv and installs laya + torch. Exits non-zero on failure.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
NEHANDA_HOME="${NEHANDA_HOME:-$HOME/.nehanda}"
|
|
7
|
+
VENV_DIR="${NEHANDA_HOME}/venv"
|
|
8
|
+
PY="${VENV_DIR}/bin/python"
|
|
9
|
+
PIP="${VENV_DIR}/bin/pip"
|
|
10
|
+
MARKER="${NEHANDA_HOME}/.laya-ready"
|
|
11
|
+
|
|
12
|
+
mkdir -p "$NEHANDA_HOME"
|
|
13
|
+
|
|
14
|
+
if [[ ! -x "$PY" ]]; then
|
|
15
|
+
echo "[laya-env] creating venv at $VENV_DIR" >&2
|
|
16
|
+
python3 -m venv "$VENV_DIR"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# shellcheck disable=SC1091
|
|
20
|
+
source "${VENV_DIR}/bin/activate"
|
|
21
|
+
|
|
22
|
+
"$PIP" install --upgrade pip --quiet
|
|
23
|
+
|
|
24
|
+
need_install=0
|
|
25
|
+
if ! "$PY" -c "import laya, torch" 2>/dev/null; then
|
|
26
|
+
need_install=1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ "$need_install" -eq 1 ]]; then
|
|
30
|
+
echo "[laya-env] installing laya + torch (~808 MB download — this is a one-time setup, please wait)" >&2
|
|
31
|
+
"$PIP" install 'laya' 'torch' --progress-bar on
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Hard verify imports
|
|
35
|
+
"$PY" - <<'PY'
|
|
36
|
+
import sys
|
|
37
|
+
import laya
|
|
38
|
+
import torch
|
|
39
|
+
print(f"[laya-env] laya={getattr(laya, '__version__', 'ok')} torch={torch.__version__}", file=sys.stderr)
|
|
40
|
+
if sys.platform == "darwin":
|
|
41
|
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
42
|
+
print("[laya-env] MPS available", file=sys.stderr)
|
|
43
|
+
else:
|
|
44
|
+
print("[laya-env] WARN: MPS not available; Laya will use CPU (slower)", file=sys.stderr)
|
|
45
|
+
PY
|
|
46
|
+
|
|
47
|
+
# Smoke: Router constructible (model weights download on first predict/load)
|
|
48
|
+
"$PY" - <<'PY'
|
|
49
|
+
from laya import Router
|
|
50
|
+
Router()
|
|
51
|
+
print("[laya-env] Router() constructible", file=sys.stderr)
|
|
52
|
+
PY
|
|
53
|
+
|
|
54
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER"
|
|
55
|
+
echo "$PY"
|