pythia-plsql 0.1.3__tar.gz → 0.2.2__tar.gz
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.
- {pythia_plsql-0.1.3/scripts/pythia_plsql.egg-info → pythia_plsql-0.2.2}/PKG-INFO +17 -6
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/README.md +16 -5
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/pyproject.toml +1 -1
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia.py +131 -26
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2/scripts/pythia_plsql.egg-info}/PKG-INFO +17 -6
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/SOURCES.txt +10 -10
- {pythia_plsql-0.1.3/skills/plsql-apply → pythia_plsql-0.2.2/skills/pythia-apply}/SKILL.md +5 -5
- {pythia_plsql-0.1.3/skills/plsql-explore → pythia_plsql-0.2.2/skills/pythia-explore}/SKILL.md +2 -2
- {pythia_plsql-0.1.3/skills/plsql-impact → pythia_plsql-0.2.2/skills/pythia-impact}/SKILL.md +4 -4
- {pythia_plsql-0.1.3/skills/plsql-review → pythia_plsql-0.2.2/skills/pythia-review}/SKILL.md +4 -4
- {pythia_plsql-0.1.3/skills/plsql-setup → pythia_plsql-0.2.2/skills/pythia-setup}/SKILL.md +11 -4
- {pythia_plsql-0.1.3/skills/plsql-skill-author → pythia_plsql-0.2.2/skills/pythia-skill-author}/SKILL.md +2 -2
- {pythia_plsql-0.1.3/skills/plsql-write → pythia_plsql-0.2.2/skills/pythia-write}/SKILL.md +5 -5
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/tests/test_install.py +54 -10
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/tests/test_phase1.py +1 -2
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/tests/test_phase3.py +102 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/tests/test_phase5.py +8 -8
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/LICENSE +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/compile-errors.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/dependencies.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/impact.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/invalid-objects.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/name-occupants.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/object-source.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/plscope-enabled.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/plscope-statements.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/plscope-usages.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/session-privileges.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/similar-candidates.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/queries/source.sql +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/setup.cfg +0 -0
- {pythia_plsql-0.1.3/skills/plsql-explore → pythia_plsql-0.2.2/skills/pythia-explore}/reference/data-dictionary.md +0 -0
- {pythia_plsql-0.1.3/skills/plsql-review → pythia_plsql-0.2.2/skills/pythia-review}/reference/antipatterns.md +0 -0
- {pythia_plsql-0.1.3/skills/plsql-write → pythia_plsql-0.2.2/skills/pythia-write}/reference/patterns.md +0 -0
- {pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/tests/test_phase2.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/thaildhe172591/pythia
|
|
@@ -82,7 +82,8 @@ pythia check # fill in connections.json first, then verify
|
|
|
82
82
|
The pip package is the whole kit: with Node.js present, `pythia install`
|
|
83
83
|
runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
|
|
84
84
|
for internal mirrors) — without Node it copies the bundled skill pack into
|
|
85
|
-
`.
|
|
85
|
+
`.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
|
|
86
|
+
read (one copy, so no skill shows up twice). Skills alone: `npx skills add
|
|
86
87
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
87
88
|
|
|
88
89
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -121,7 +122,17 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
121
122
|
**The account is the real security layer** — the policy file is an application-side
|
|
122
123
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
123
124
|
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
124
|
-
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
128
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
132
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
133
|
+
hand with
|
|
134
|
+
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
135
|
+
works just as well.
|
|
125
136
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
126
137
|
|
|
127
138
|
Per-group write policy, `.pythia/policy.json` (defaults shown):
|
|
@@ -146,10 +157,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
|
|
|
146
157
|
|
|
147
158
|
Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
|
|
148
159
|
|
|
149
|
-
`
|
|
150
|
-
`
|
|
160
|
+
`pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
|
|
161
|
+
`pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
|
|
151
162
|
developer sees the preview and approves in chat before anything is written) ·
|
|
152
|
-
`
|
|
163
|
+
`pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
|
|
153
164
|
workflow as a new skill, mined from the live schema).
|
|
154
165
|
|
|
155
166
|
## Compatibility
|
|
@@ -68,7 +68,8 @@ pythia check # fill in connections.json first, then verify
|
|
|
68
68
|
The pip package is the whole kit: with Node.js present, `pythia install`
|
|
69
69
|
runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
|
|
70
70
|
for internal mirrors) — without Node it copies the bundled skill pack into
|
|
71
|
-
`.
|
|
71
|
+
`.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
|
|
72
|
+
read (one copy, so no skill shows up twice). Skills alone: `npx skills add
|
|
72
73
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
73
74
|
|
|
74
75
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -107,7 +108,17 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
107
108
|
**The account is the real security layer** — the policy file is an application-side
|
|
108
109
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
109
110
|
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
110
|
-
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
114
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
118
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
119
|
+
hand with
|
|
120
|
+
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
121
|
+
works just as well.
|
|
111
122
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
112
123
|
|
|
113
124
|
Per-group write policy, `.pythia/policy.json` (defaults shown):
|
|
@@ -132,10 +143,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
|
|
|
132
143
|
|
|
133
144
|
Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
|
|
134
145
|
|
|
135
|
-
`
|
|
136
|
-
`
|
|
146
|
+
`pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
|
|
147
|
+
`pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
|
|
137
148
|
developer sees the preview and approves in chat before anything is written) ·
|
|
138
|
-
`
|
|
149
|
+
`pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
|
|
139
150
|
workflow as a new skill, mined from the live schema).
|
|
140
151
|
|
|
141
152
|
## Compatibility
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythia-plsql"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -536,7 +536,7 @@ POLICY_DEFAULTS = {"plsql_source": "confirm", "data_dml": "deny",
|
|
|
536
536
|
"structural": "deny", "grants": "deny", "session": "allow"}
|
|
537
537
|
|
|
538
538
|
ROLLBACK_TABLE = """\
|
|
539
|
-
Is rollback real? (this table also appears in README.md and
|
|
539
|
+
Is rollback real? (this table also appears in README.md and pythia-apply)
|
|
540
540
|
plsql_source Yes - completely. Source is recoverable from ALL_SOURCE.
|
|
541
541
|
data_dml No. After commit only Flashback Query remains, within undo retention.
|
|
542
542
|
structural Almost never. DROP COLUMN is permanent; a dropped table may be in the Recycle Bin.
|
|
@@ -586,6 +586,17 @@ def load_policy(root):
|
|
|
586
586
|
sys.exit(f"Cannot parse {path}: {e}")
|
|
587
587
|
|
|
588
588
|
|
|
589
|
+
def load_settings(root):
|
|
590
|
+
"""Optional .pythia/settings.json — an absent file means defaults."""
|
|
591
|
+
path = pathlib.Path(root) / CONFIG_DIR / "settings.json"
|
|
592
|
+
if not path.is_file():
|
|
593
|
+
return {}
|
|
594
|
+
try:
|
|
595
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
596
|
+
except ValueError as e:
|
|
597
|
+
sys.exit(f"Cannot parse {path}: {e}")
|
|
598
|
+
|
|
599
|
+
|
|
589
600
|
def journal_root(root):
|
|
590
601
|
return pathlib.Path(root) / CONFIG_DIR / "journal"
|
|
591
602
|
|
|
@@ -1014,6 +1025,19 @@ def privilege_warning(conn, schema, conn_user):
|
|
|
1014
1025
|
this session is running with more power than the task needs."""
|
|
1015
1026
|
_, rows = run_query(conn, load_query("session-privileges.sql"))
|
|
1016
1027
|
dangerous = [r[0] for r in rows]
|
|
1028
|
+
with conn.cursor() as cur:
|
|
1029
|
+
cur.execute("select sys_context('userenv','proxy_user') from dual")
|
|
1030
|
+
prow = cur.fetchall()
|
|
1031
|
+
proxy = prow[0][0] if prow and prow[0] and prow[0][0] else None
|
|
1032
|
+
if proxy:
|
|
1033
|
+
# sanctioned least-privilege entrance; any excess power is the
|
|
1034
|
+
# OWNER's, inherited by every proxied session — say that
|
|
1035
|
+
if not dangerous:
|
|
1036
|
+
return None
|
|
1037
|
+
return (f"! Proxy session ({proxy} → {schema}), but the owner holds "
|
|
1038
|
+
f"{', '.join(dangerous[:3])}" + ("…" if len(dangerous) > 3 else "")
|
|
1039
|
+
+ ":\n every proxied session inherits this — trim the "
|
|
1040
|
+
"owner's grants; pythia policy explains what is at stake.")
|
|
1017
1041
|
owner = bool(conn_user) and conn_user.upper() == schema
|
|
1018
1042
|
if not owner and not dangerous:
|
|
1019
1043
|
return None
|
|
@@ -1151,6 +1175,14 @@ def run_apply(conn, schema, ns, file_text, origin=None):
|
|
|
1151
1175
|
return 0
|
|
1152
1176
|
|
|
1153
1177
|
# 4. APPLY
|
|
1178
|
+
if group == "plsql_source" and load_settings(ns.project_root).get(
|
|
1179
|
+
"plscope_on_apply", True):
|
|
1180
|
+
# session-scoped: every object applied through pythia builds the
|
|
1181
|
+
# PL/Scope index as a side effect; opt out with
|
|
1182
|
+
# {"plscope_on_apply": false} in .pythia/settings.json
|
|
1183
|
+
with conn.cursor() as cur:
|
|
1184
|
+
cur.execute("ALTER SESSION SET plscope_settings = "
|
|
1185
|
+
"'IDENTIFIERS:ALL, STATEMENTS:ALL'")
|
|
1154
1186
|
with conn.cursor() as cur:
|
|
1155
1187
|
cur.execute(stmt)
|
|
1156
1188
|
|
|
@@ -1264,9 +1296,22 @@ def cmd_journal(conn, schema, ns):
|
|
|
1264
1296
|
state = "applied" if meta.get("applied") else "preview"
|
|
1265
1297
|
print(f"{eid} [{state}]")
|
|
1266
1298
|
return
|
|
1299
|
+
if ns.action == "prune":
|
|
1300
|
+
import shutil
|
|
1301
|
+
removed = 0
|
|
1302
|
+
for eid in list_journal_entries(root):
|
|
1303
|
+
if not read_journal_entry(root, eid)["meta"].get("applied"):
|
|
1304
|
+
shutil.rmtree(journal_root(root) / eid)
|
|
1305
|
+
removed += 1
|
|
1306
|
+
if ns.json:
|
|
1307
|
+
print(json.dumps({"pruned": removed}))
|
|
1308
|
+
else:
|
|
1309
|
+
print(f"-- pruned {removed} preview-only entries; applied entries "
|
|
1310
|
+
"(the real snapshots) are all kept")
|
|
1311
|
+
return
|
|
1267
1312
|
if not ns.id:
|
|
1268
1313
|
sys.exit(f"Usage: {invocation()} journal "
|
|
1269
|
-
"{list | show <id> | diff <id> | export <id> | restore <id>}")
|
|
1314
|
+
"{list | show <id> | diff <id> | export <id> | prune | restore <id>}")
|
|
1270
1315
|
e = read_journal_entry(root, ns.id)
|
|
1271
1316
|
if ns.action == "show":
|
|
1272
1317
|
print(json.dumps(e["meta"], indent=2))
|
|
@@ -1310,7 +1355,7 @@ AGENT_USER_SQL = """\
|
|
|
1310
1355
|
-- privileges, utility grants — the agent needs none of them to develop
|
|
1311
1356
|
-- PL/SQL, and every extra grant widens the blast radius.
|
|
1312
1357
|
|
|
1313
|
-
|
|
1358
|
+
{create_line}
|
|
1314
1359
|
GRANT CREATE SESSION TO {agent};
|
|
1315
1360
|
ALTER USER {owner} GRANT CONNECT THROUGH {agent};
|
|
1316
1361
|
|
|
@@ -1320,6 +1365,23 @@ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
|
|
|
1320
1365
|
"""
|
|
1321
1366
|
|
|
1322
1367
|
|
|
1368
|
+
def agent_user_sql(owner, agent, password, exists):
|
|
1369
|
+
"""exists True: the user is already there — CREATE would be ORA-01920,
|
|
1370
|
+
so reset the password and unlock in one statement (harmless if it was
|
|
1371
|
+
never locked). exists None: no database to ask — emit CREATE plus the
|
|
1372
|
+
fallback as a comment so the DBA can pick."""
|
|
1373
|
+
if exists:
|
|
1374
|
+
line = (f'ALTER USER {agent} IDENTIFIED BY "{password}" ACCOUNT '
|
|
1375
|
+
f"UNLOCK; -- user already exists; CREATE would be ORA-01920")
|
|
1376
|
+
else:
|
|
1377
|
+
line = f'CREATE USER {agent} IDENTIFIED BY "{password}";'
|
|
1378
|
+
if exists is None:
|
|
1379
|
+
line += ("\n-- (if the user already exists — ORA-01920 — run "
|
|
1380
|
+
"instead:\n-- ALTER USER "
|
|
1381
|
+
f'{agent} IDENTIFIED BY "{password}" ACCOUNT UNLOCK;)')
|
|
1382
|
+
return AGENT_USER_SQL.format(owner=owner, agent=agent, create_line=line)
|
|
1383
|
+
|
|
1384
|
+
|
|
1323
1385
|
def agent_password():
|
|
1324
1386
|
"""Random password satisfying common Oracle verify functions: letter
|
|
1325
1387
|
first, upper+lower+digit+special, 16+ chars. Always quoted in SQL."""
|
|
@@ -1358,18 +1420,51 @@ def cmd_agent_user(conn, schema, ns):
|
|
|
1358
1420
|
f"({base['user']}) — nothing to set up.")
|
|
1359
1421
|
agent = (ns.name or f"{owner}_AGENT").upper()
|
|
1360
1422
|
password = agent_password()
|
|
1361
|
-
|
|
1423
|
+
exists, dangerous = None, None
|
|
1424
|
+
if conn is not None:
|
|
1425
|
+
_, rows = run_query(conn, "select username from all_users "
|
|
1426
|
+
"where username = :n", {"n": agent})
|
|
1427
|
+
exists = bool(rows)
|
|
1428
|
+
_, prows = run_query(conn, load_query("session-privileges.sql"))
|
|
1429
|
+
dangerous = [r[0] for r in prows]
|
|
1430
|
+
sql = agent_user_sql(owner, agent, password, exists)
|
|
1431
|
+
saved = None
|
|
1362
1432
|
if ns.save:
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1433
|
+
saved = save_agent_connection(root, base_name, base, agent, password)
|
|
1434
|
+
if ns.json:
|
|
1435
|
+
print(json.dumps({
|
|
1436
|
+
"owner": owner, "agent": agent, "password": password, "sql": sql,
|
|
1437
|
+
"agent_user_exists": exists, "saved_connection": saved,
|
|
1438
|
+
"owner_connection": base_name,
|
|
1439
|
+
"check_will_warn": bool(dangerous) if dangerous is not None else None,
|
|
1440
|
+
"owner_dangerous_privs": dangerous,
|
|
1441
|
+
"next": ["have a DBA run the sql", f"{invocation()} check"]}))
|
|
1442
|
+
return
|
|
1443
|
+
print(sql)
|
|
1444
|
+
print("-- The password is regenerated on EVERY run — only the SQL from")
|
|
1445
|
+
print("-- this exact run matches what --save writes. One run, not two.")
|
|
1446
|
+
if dangerous:
|
|
1447
|
+
print(f"\n! Even after this, {invocation()} check will STILL warn: "
|
|
1448
|
+
f"the owner {owner} holds\n {', '.join(dangerous[:3])}"
|
|
1449
|
+
+ ("…" if len(dangerous) > 3 else "")
|
|
1450
|
+
+ " — every proxy session inherits it.\n"
|
|
1451
|
+
" Trim the owner's grants to reach the no-warning goal state "
|
|
1452
|
+
"(see README, Security).")
|
|
1453
|
+
elif dangerous is not None:
|
|
1454
|
+
print(f"\nOwner grants look clean: after the DBA runs this, "
|
|
1455
|
+
f"{invocation()} check should pass with no warning.")
|
|
1456
|
+
if saved:
|
|
1457
|
+
print(f"\nSaved connection '{saved}' (now the default) in "
|
|
1458
|
+
f"{pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME} — its "
|
|
1459
|
+
f"password matches the SQL above;\n"
|
|
1366
1460
|
f"the owner entry '{base_name}' is untouched — switch back "
|
|
1367
1461
|
f"any time with --conn {base_name}.")
|
|
1368
|
-
print(f"\nNext:
|
|
1462
|
+
print(f"\nNext: relay the SQL above to the developer verbatim for a "
|
|
1463
|
+
f"DBA to run, then: {invocation()} check")
|
|
1369
1464
|
else:
|
|
1370
|
-
print(f"--
|
|
1371
|
-
f"connections.json as '{base_name}_agent'
|
|
1372
|
-
f"
|
|
1465
|
+
print(f"-- Nothing was saved. Use --save to also write this "
|
|
1466
|
+
f"credential to connections.json as '{base_name}_agent' in "
|
|
1467
|
+
f"the same run.")
|
|
1373
1468
|
|
|
1374
1469
|
|
|
1375
1470
|
# unistr escapes. Single quote doubles to '' per SQL (a \' is ORA-01756 for
|
|
@@ -1460,20 +1555,31 @@ def run_skills_add(source, interactive=False):
|
|
|
1460
1555
|
return subprocess.run(cmd).returncode
|
|
1461
1556
|
|
|
1462
1557
|
|
|
1558
|
+
LEGACY_SKILLS = ("plsql-setup", "plsql-explore", "plsql-impact",
|
|
1559
|
+
"plsql-write", "plsql-apply", "plsql-review",
|
|
1560
|
+
"plsql-skill-author")
|
|
1561
|
+
|
|
1562
|
+
|
|
1463
1563
|
def copy_bundled_skills(root):
|
|
1464
|
-
"""No-Node fallback: copy the wheel-bundled pack into
|
|
1465
|
-
|
|
1466
|
-
|
|
1564
|
+
"""No-Node fallback: copy the wheel-bundled pack into .agents/skills/
|
|
1565
|
+
ONLY — the universal layout that Claude Code, Codex, Cursor, Copilot and
|
|
1566
|
+
friends all read. A second copy in .claude/skills/ would show every
|
|
1567
|
+
skill twice in Claude Code's menu. The copy merges: only the pack's own
|
|
1568
|
+
skills are refreshed, anything else in the directory is untouched.
|
|
1569
|
+
Stale copies under the pack's old plsql-* names are removed from both
|
|
1570
|
+
conventional roots (real directories only, never symlinks)."""
|
|
1467
1571
|
import shutil
|
|
1468
|
-
|
|
1469
|
-
for
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1572
|
+
dest_root = pathlib.Path(root) / ".agents" / "skills"
|
|
1573
|
+
for pack in sorted(SKILLS_DIR.iterdir()):
|
|
1574
|
+
if not (pack / "SKILL.md").is_file():
|
|
1575
|
+
continue
|
|
1576
|
+
shutil.copytree(pack, dest_root / pack.name, dirs_exist_ok=True)
|
|
1577
|
+
for rel in (".agents/skills", ".claude/skills"):
|
|
1578
|
+
for old_name in LEGACY_SKILLS:
|
|
1579
|
+
legacy = pathlib.Path(root) / rel / old_name
|
|
1580
|
+
if legacy.is_dir() and not legacy.is_symlink():
|
|
1581
|
+
shutil.rmtree(legacy)
|
|
1582
|
+
return [dest_root]
|
|
1477
1583
|
|
|
1478
1584
|
|
|
1479
1585
|
def cmd_install(conn, schema, ns):
|
|
@@ -1503,8 +1609,7 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
|
|
|
1503
1609
|
"conventions": cmd_conventions, "install": cmd_install,
|
|
1504
1610
|
"unistr": cmd_unistr, "agent-user": cmd_agent_user}
|
|
1505
1611
|
|
|
1506
|
-
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"
|
|
1507
|
-
"agent-user"}
|
|
1612
|
+
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"}
|
|
1508
1613
|
|
|
1509
1614
|
|
|
1510
1615
|
# --- CLI ---------------------------------------------------------------------
|
|
@@ -1584,7 +1689,7 @@ def build_parser():
|
|
|
1584
1689
|
s = sub.add_parser("journal", parents=[common()],
|
|
1585
1690
|
help="list, inspect, export and restore write snapshots")
|
|
1586
1691
|
s.add_argument("action", nargs="?",
|
|
1587
|
-
choices=["list", "show", "diff", "export", "restore"],
|
|
1692
|
+
choices=["list", "show", "diff", "export", "prune", "restore"],
|
|
1588
1693
|
default="list")
|
|
1589
1694
|
s.add_argument("id", nargs="?")
|
|
1590
1695
|
s.add_argument("--what", choices=["after", "before", "restore"],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/thaildhe172591/pythia
|
|
@@ -82,7 +82,8 @@ pythia check # fill in connections.json first, then verify
|
|
|
82
82
|
The pip package is the whole kit: with Node.js present, `pythia install`
|
|
83
83
|
runs `npx skills add` (77 agents, symlinked updates; `--source <git-url>`
|
|
84
84
|
for internal mirrors) — without Node it copies the bundled skill pack into
|
|
85
|
-
`.
|
|
85
|
+
`.agents/skills/`, the universal layout Claude Code, Codex and Cursor all
|
|
86
|
+
read (one copy, so no skill shows up twice). Skills alone: `npx skills add
|
|
86
87
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
87
88
|
|
|
88
89
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -121,7 +122,17 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
121
122
|
**The account is the real security layer** — the policy file is an application-side
|
|
122
123
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
123
124
|
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
124
|
-
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
128
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
132
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
133
|
+
hand with
|
|
134
|
+
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
135
|
+
works just as well.
|
|
125
136
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
126
137
|
|
|
127
138
|
Per-group write policy, `.pythia/policy.json` (defaults shown):
|
|
@@ -146,10 +157,10 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
|
|
|
146
157
|
|
|
147
158
|
Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
|
|
148
159
|
|
|
149
|
-
`
|
|
150
|
-
`
|
|
160
|
+
`pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
|
|
161
|
+
`pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
|
|
151
162
|
developer sees the preview and approves in chat before anything is written) ·
|
|
152
|
-
`
|
|
163
|
+
`pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
|
|
153
164
|
workflow as a new skill, mined from the live schema).
|
|
154
165
|
|
|
155
166
|
## Compatibility
|
|
@@ -20,16 +20,16 @@ scripts/pythia_plsql.egg-info/dependency_links.txt
|
|
|
20
20
|
scripts/pythia_plsql.egg-info/entry_points.txt
|
|
21
21
|
scripts/pythia_plsql.egg-info/requires.txt
|
|
22
22
|
scripts/pythia_plsql.egg-info/top_level.txt
|
|
23
|
-
skills/
|
|
24
|
-
skills/
|
|
25
|
-
skills/
|
|
26
|
-
skills/
|
|
27
|
-
skills/
|
|
28
|
-
skills/
|
|
29
|
-
skills/
|
|
30
|
-
skills/
|
|
31
|
-
skills/
|
|
32
|
-
skills/
|
|
23
|
+
skills/pythia-apply/SKILL.md
|
|
24
|
+
skills/pythia-explore/SKILL.md
|
|
25
|
+
skills/pythia-explore/reference/data-dictionary.md
|
|
26
|
+
skills/pythia-impact/SKILL.md
|
|
27
|
+
skills/pythia-review/SKILL.md
|
|
28
|
+
skills/pythia-review/reference/antipatterns.md
|
|
29
|
+
skills/pythia-setup/SKILL.md
|
|
30
|
+
skills/pythia-skill-author/SKILL.md
|
|
31
|
+
skills/pythia-write/SKILL.md
|
|
32
|
+
skills/pythia-write/reference/patterns.md
|
|
33
33
|
tests/test_install.py
|
|
34
34
|
tests/test_phase1.py
|
|
35
35
|
tests/test_phase2.py
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-apply
|
|
3
3
|
description: Use when a PL/SQL change is ready to reach the database - applying a CREATE OR REPLACE, restoring from the journal, or any other write. Runs the six-step flow through pythia apply and enforces the gate - the developer sees the preview and approves in chat before anything is written, and a broken result is never reported as success.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Applying PL/SQL Changes
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-apply — I'll preview the change first."
|
|
9
9
|
|
|
10
10
|
DDL in Oracle commits itself. There is no transaction to roll back — the
|
|
11
11
|
snapshot pythia takes before writing is the only undo that exists. This skill
|
|
@@ -24,7 +24,7 @@ dependents.
|
|
|
24
24
|
## The Workflow
|
|
25
25
|
|
|
26
26
|
**Before step 1:** if this conversation has not yet seen a standalone impact
|
|
27
|
-
analysis for this object, run `
|
|
27
|
+
analysis for this object, run `pythia-impact` first. The `impact:` line inside
|
|
28
28
|
apply's preview is confirmation of a number you already knew — if it is the
|
|
29
29
|
first time anyone sees it, a step was skipped.
|
|
30
30
|
|
|
@@ -114,8 +114,8 @@ the stakes are highest.
|
|
|
114
114
|
|
|
115
115
|
## When NOT to use this skill
|
|
116
116
|
|
|
117
|
-
- Reading or exploring — use `
|
|
118
|
-
- Judging blast radius before editing — use `
|
|
117
|
+
- Reading or exploring — use `pythia-explore`.
|
|
118
|
+
- Judging blast radius before editing — use `pythia-impact` (always run it
|
|
119
119
|
before proposing a change; apply's preview is confirmation, not discovery).
|
|
120
120
|
- Editing files the developer has not asked to land on the database yet.
|
|
121
121
|
|
{pythia_plsql-0.1.3/skills/plsql-explore → pythia_plsql-0.2.2/skills/pythia-explore}/SKILL.md
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-explore
|
|
3
3
|
description: Use when you need to understand anything in an Oracle schema - finding objects, reading PL/SQL source, signatures, table columns, DDL, searching code, or asking who uses what. The database is the only source of truth; repo dumps and exports drift and lie.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Exploring the Schema
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-explore — asking the database directly."
|
|
9
9
|
|
|
10
10
|
## The principle: ask the database, never the dump
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-impact
|
|
3
3
|
description: Use before proposing or writing any change to an Oracle object - a procedure, package, function, trigger, view, or table. Impact analysis comes first, because what depends on an object decides how careful the change must be, and Oracle invalidates dependents in cascade.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Impact Before Change
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-impact to measure the blast radius first."
|
|
9
9
|
|
|
10
10
|
Changing an Oracle object recompiles or invalidates everything that depends
|
|
11
11
|
on it — immediately, schema-wide, for every user of a shared database. The
|
|
@@ -51,6 +51,6 @@ program that would be invalidated by an `ALTER`.
|
|
|
51
51
|
|
|
52
52
|
## Hand-off
|
|
53
53
|
|
|
54
|
-
Impact known and acceptable → `
|
|
55
|
-
the codebase's conventions → `
|
|
54
|
+
Impact known and acceptable → `pythia-write` to write the change following
|
|
55
|
+
the codebase's conventions → `pythia-apply` to land it with preview and
|
|
56
56
|
verification.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-review
|
|
3
3
|
description: Use when reviewing PL/SQL - a changed procedure or package, a proposed file, or an object suspected of causing trouble. Combines the database's own correctness signals with the antipattern checklist, and reports findings anchored to line numbers.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Reviewing PL/SQL
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-review — checking the database's signals first."
|
|
9
9
|
|
|
10
10
|
Review in two passes: what the database *knows* is wrong, then what the
|
|
11
11
|
checklist says is *likely* wrong. Machine signals first — they are free and
|
|
@@ -45,6 +45,6 @@ style).
|
|
|
45
45
|
|
|
46
46
|
## When NOT to use this skill
|
|
47
47
|
|
|
48
|
-
- Reviewing whether a change is *safe to apply* — that is `
|
|
49
|
-
plus `
|
|
48
|
+
- Reviewing whether a change is *safe to apply* — that is `pythia-impact`
|
|
49
|
+
plus `pythia-apply`'s preview; this skill judges the code itself.
|
|
50
50
|
- Reviewing non-PL/SQL application code — outside this skill's scope.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-setup
|
|
3
3
|
description: Use when setting pythia up for a project or a machine - writing connections.json, creating a least-privilege agent user, wiring the SQLcl MCP server, or diagnosing a connection that will not open. Also the place to start when check prints a privilege warning.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Setting Up
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-setup to configure the database access."
|
|
9
9
|
|
|
10
10
|
Set up in this order: connection first (everything else needs it), the
|
|
11
11
|
least-privilege user second (the only protection that cannot be bypassed),
|
|
@@ -41,8 +41,15 @@ connects *through* the schema owner. The agent never learns the owner's
|
|
|
41
41
|
password, revocation is one statement, the audit trail shows who really
|
|
42
42
|
connected, and the blast radius is the one development schema.
|
|
43
43
|
|
|
44
|
-
Run `
|
|
45
|
-
|
|
44
|
+
Run `pythia agent-user --save` — ONE run, in the project directory. It
|
|
45
|
+
prints the three-statement proxy SQL with a generated password and saves
|
|
46
|
+
the matching credential as connection `<conn>_agent` (the new default; the
|
|
47
|
+
owner entry stays untouched). The password is regenerated on every run, so
|
|
48
|
+
never preview first and save later — the SQL and the saved config must
|
|
49
|
+
come from the same run. Relay the SQL to the developer verbatim for a DBA
|
|
50
|
+
to execute, then verify with `pythia check`. `--json` gives the same
|
|
51
|
+
result machine-readable. Manual alternative:
|
|
52
|
+
`examples/agent-user-setup.example.sql`, names adapted.
|
|
46
53
|
|
|
47
54
|
Oracle's own guidance for LLM access, follow it: grant minimum privileges,
|
|
48
55
|
never point an LLM at a production database, audit its activity regularly.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-skill-author
|
|
3
3
|
description: Use when the developer wants their own way of working captured as a new skill - "make a skill for how we do X", a house convention or preference worth teaching every future session, a team ritual, or the same correction arriving for the second time. Interviews the developer, mines the real conventions from the database, and writes a skill in this pack's format.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Authoring a New Skill
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-skill-author — let's capture how you actually work."
|
|
9
9
|
|
|
10
10
|
A skill is a decision captured so it never has to be re-argued. Capture what
|
|
11
11
|
the team *actually does* — with evidence from the database — not what anyone
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pythia-write
|
|
3
3
|
description: Use when writing or modifying PL/SQL source - a procedure, function, package, trigger, or view - after impact is known. The codebase's conventions already exist; copy them instead of inventing style, and anchor every type to the database's reality.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Writing PL/SQL
|
|
7
7
|
|
|
8
|
-
**Announce at start:** "Using
|
|
8
|
+
**Announce at start:** "Using pythia-write — mining the codebase's conventions first."
|
|
9
9
|
|
|
10
10
|
**Before anything:** if the project has `.pythia/conventions.md`, read it —
|
|
11
11
|
house rules there outrank every generic pattern below, and
|
|
@@ -49,7 +49,7 @@ propose style changes to the developer separately, never silently.
|
|
|
49
49
|
|
|
50
50
|
## When NOT to use this skill
|
|
51
51
|
|
|
52
|
-
- Understanding existing code → `
|
|
53
|
-
- Measuring what a change breaks → `
|
|
54
|
-
- Landing the file on the database → `
|
|
52
|
+
- Understanding existing code → `pythia-explore`.
|
|
53
|
+
- Measuring what a change breaks → `pythia-impact` (must already be done).
|
|
54
|
+
- Landing the file on the database → `pythia-apply`, always — never
|
|
55
55
|
`run-sql`, never `sqlplus`.
|
|
@@ -35,8 +35,11 @@ def test_scaffold_creates_config_once_and_never_clobbers():
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def test_missing_npx_falls_back_to_the_bundled_pack():
|
|
38
|
-
"""No Node -> the whole kit still installs
|
|
39
|
-
|
|
38
|
+
"""No Node -> the whole kit still installs. One target only
|
|
39
|
+
(.agents/skills, the universal layout) — a second copy in
|
|
40
|
+
.claude/skills would double every skill in Claude Code's menu. The
|
|
41
|
+
copy merges: foreign skills survive, and stale plsql-* copies of the
|
|
42
|
+
pack's old names are cleaned from both conventional roots."""
|
|
40
43
|
import shutil
|
|
41
44
|
old = shutil.which
|
|
42
45
|
shutil.which = lambda name: None
|
|
@@ -45,14 +48,23 @@ def test_missing_npx_falls_back_to_the_bundled_pack():
|
|
|
45
48
|
finally:
|
|
46
49
|
shutil.which = old
|
|
47
50
|
with tempfile.TemporaryDirectory() as td:
|
|
51
|
+
agents = pathlib.Path(td) / ".agents" / "skills"
|
|
52
|
+
claude = pathlib.Path(td) / ".claude" / "skills"
|
|
53
|
+
# pre-existing content: a custom skill, and stale old-name copies
|
|
54
|
+
(agents / "my-team-skill").mkdir(parents=True)
|
|
55
|
+
(agents / "my-team-skill" / "SKILL.md").write_text("x", encoding="utf-8")
|
|
56
|
+
(agents / "plsql-apply").mkdir()
|
|
57
|
+
(claude / "plsql-review").mkdir(parents=True)
|
|
48
58
|
targets = pythia.copy_bundled_skills(td)
|
|
49
|
-
assert
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
assert targets == [agents]
|
|
60
|
+
assert (agents / "pythia-apply" / "SKILL.md").is_file()
|
|
61
|
+
assert (agents / "pythia-review" / "reference" / "antipatterns.md").is_file()
|
|
62
|
+
assert not (claude / "pythia-apply").exists() # single target
|
|
63
|
+
assert (agents / "my-team-skill" / "SKILL.md").read_text(
|
|
64
|
+
encoding="utf-8") == "x" # merge, not wipe
|
|
65
|
+
assert not (agents / "plsql-apply").exists() # legacy cleaned
|
|
66
|
+
assert not (claude / "plsql-review").exists() # in both roots
|
|
67
|
+
pythia.copy_bundled_skills(td) # idempotent
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def test_invocation_names_the_dash_m_form():
|
|
@@ -72,7 +84,39 @@ def test_invocation_names_the_dash_m_form():
|
|
|
72
84
|
|
|
73
85
|
def test_pack_dirs_resolve_in_the_source_layout():
|
|
74
86
|
assert (pythia.QUERY_DIR / "impact.sql").is_file()
|
|
75
|
-
assert (pythia.SKILLS_DIR / "
|
|
87
|
+
assert (pythia.SKILLS_DIR / "pythia-apply" / "SKILL.md").is_file()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_agent_user_json_saves_the_same_password_it_prints():
|
|
91
|
+
"""The agent flow: one --json --save run must be self-consistent — the
|
|
92
|
+
password inside the SQL is the password written to connections.json."""
|
|
93
|
+
import argparse
|
|
94
|
+
import contextlib
|
|
95
|
+
import io
|
|
96
|
+
import os
|
|
97
|
+
with tempfile.TemporaryDirectory() as td:
|
|
98
|
+
root = pathlib.Path(td)
|
|
99
|
+
(root / ".pythia").mkdir()
|
|
100
|
+
(root / ".pythia" / "connections.json").write_text(json.dumps(
|
|
101
|
+
{"dev": {"host": "h", "port": 1521, "service_name": "s",
|
|
102
|
+
"user": "OWNER", "password": "p", "schema": "OWNER"}}),
|
|
103
|
+
encoding="utf-8")
|
|
104
|
+
cwd = os.getcwd()
|
|
105
|
+
os.chdir(td)
|
|
106
|
+
try:
|
|
107
|
+
ns = argparse.Namespace(conn=None, json=True, save=True, name=None)
|
|
108
|
+
buf = io.StringIO()
|
|
109
|
+
with contextlib.redirect_stdout(buf):
|
|
110
|
+
pythia.cmd_agent_user(None, None, ns)
|
|
111
|
+
d = json.loads(buf.getvalue())
|
|
112
|
+
assert d["saved_connection"] == "dev_agent"
|
|
113
|
+
assert d["password"] in d["sql"]
|
|
114
|
+
cfg = json.loads((root / ".pythia" / "connections.json")
|
|
115
|
+
.read_text(encoding="utf-8"))
|
|
116
|
+
assert cfg["dev_agent"]["password"] == d["password"]
|
|
117
|
+
assert cfg["dev"]["password"] == "p" # owner untouched
|
|
118
|
+
finally:
|
|
119
|
+
os.chdir(cwd)
|
|
76
120
|
|
|
77
121
|
|
|
78
122
|
def test_one_version_across_every_manifest():
|
|
@@ -254,8 +254,7 @@ def test_to_unistr():
|
|
|
254
254
|
|
|
255
255
|
|
|
256
256
|
def test_agent_user_sql_is_least_privilege():
|
|
257
|
-
sql = pythia.
|
|
258
|
-
password="x")
|
|
257
|
+
sql = pythia.agent_user_sql("APP", "APP_AGENT", "x", exists=False)
|
|
259
258
|
assert "CREATE USER APP_AGENT" in sql
|
|
260
259
|
assert "GRANT CREATE SESSION TO APP_AGENT" in sql
|
|
261
260
|
assert "GRANT CONNECT THROUGH APP_AGENT" in sql
|
|
@@ -489,6 +489,108 @@ def test_preview_diff_ignores_the_create_header():
|
|
|
489
489
|
assert "lines changed" not in out
|
|
490
490
|
|
|
491
491
|
|
|
492
|
+
def test_apply_builds_plscope_index_by_default():
|
|
493
|
+
"""The write session sets plscope_settings before the CREATE, so every
|
|
494
|
+
object applied through pythia carries the semantic index."""
|
|
495
|
+
with tempfile.TemporaryDirectory() as td:
|
|
496
|
+
conn = FakeConn(base_script())
|
|
497
|
+
code = pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
|
|
498
|
+
NEW_FILE)
|
|
499
|
+
assert code == 0
|
|
500
|
+
stmts = [s for s, _ in conn.executed]
|
|
501
|
+
alter = [i for i, s in enumerate(stmts) if "plscope_settings" in s.lower()]
|
|
502
|
+
create = [i for i, s in enumerate(stmts)
|
|
503
|
+
if s.lstrip().lower().startswith("create")]
|
|
504
|
+
assert alter and create and alter[0] < create[0]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_apply_plscope_opt_out_via_settings():
|
|
508
|
+
with tempfile.TemporaryDirectory() as td:
|
|
509
|
+
(pathlib.Path(td) / ".pythia").mkdir()
|
|
510
|
+
(pathlib.Path(td) / ".pythia" / "settings.json").write_text(
|
|
511
|
+
'{"plscope_on_apply": false}', encoding="utf-8")
|
|
512
|
+
conn = FakeConn(base_script())
|
|
513
|
+
code = pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
|
|
514
|
+
NEW_FILE)
|
|
515
|
+
assert code == 0
|
|
516
|
+
assert not any("plscope_settings" in s.lower()
|
|
517
|
+
for s, _ in conn.executed)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def test_privilege_warning_speaks_proxy():
|
|
521
|
+
"""A proxy session inheriting ANY privileges: the warning must blame the
|
|
522
|
+
owner's grants, not the entrance; a clean proxy session warns not at all."""
|
|
523
|
+
dangerous = {"from session_privs": ([("PRIVILEGE",)],
|
|
524
|
+
[("DROP ANY TABLE",), ("SELECT ANY TABLE",)]),
|
|
525
|
+
"proxy_user": ([("P",)], [("CORE_BH_AGENT",)])}
|
|
526
|
+
msg = pythia.privilege_warning(FakeConn(dangerous), "CORE_BH",
|
|
527
|
+
"core_bh_agent[core_bh]")
|
|
528
|
+
assert "Proxy session" in msg and "CORE_BH_AGENT" in msg
|
|
529
|
+
assert "owner holds" in msg and "DROP ANY TABLE" in msg
|
|
530
|
+
clean = {"from session_privs": ([("PRIVILEGE",)], []),
|
|
531
|
+
"proxy_user": ([("P",)], [("CORE_BH_AGENT",)])}
|
|
532
|
+
assert pythia.privilege_warning(FakeConn(clean), "CORE_BH",
|
|
533
|
+
"core_bh_agent[core_bh]") is None
|
|
534
|
+
# no proxy, direct owner: the original warning stands
|
|
535
|
+
direct = {"from session_privs": ([("PRIVILEGE",)], [])}
|
|
536
|
+
assert "schema owner" in pythia.privilege_warning(
|
|
537
|
+
FakeConn(direct), "APP", "APP")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_journal_prune_keeps_applied_entries():
|
|
541
|
+
import argparse
|
|
542
|
+
import datetime
|
|
543
|
+
with tempfile.TemporaryDirectory() as td:
|
|
544
|
+
base = datetime.datetime(2026, 8, 26, 12, 0, 0)
|
|
545
|
+
pythia.write_journal_entry(td, "PROCEDURE", "P_PREVIEW", "a", "b",
|
|
546
|
+
{"applied": False}, now=base)
|
|
547
|
+
kept = pythia.write_journal_entry(
|
|
548
|
+
td, "PROCEDURE", "P_APPLIED", "a", "b", {"applied": True},
|
|
549
|
+
now=base.replace(minute=1))
|
|
550
|
+
ns = argparse.Namespace(action="prune", project_root=td, json=False,
|
|
551
|
+
id=None)
|
|
552
|
+
import contextlib
|
|
553
|
+
import io
|
|
554
|
+
buf = io.StringIO()
|
|
555
|
+
with contextlib.redirect_stdout(buf):
|
|
556
|
+
pythia.cmd_journal(None, None, ns)
|
|
557
|
+
assert "pruned 1" in buf.getvalue()
|
|
558
|
+
assert pythia.list_journal_entries(td) == [kept]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def test_agent_user_alter_form_and_warning_prediction():
|
|
562
|
+
"""DB-aware agent-user: an existing agent user gets the ALTER+UNLOCK
|
|
563
|
+
form (CREATE would be ORA-01920), and a dirty owner is called out —
|
|
564
|
+
check will STILL warn — before the DBA runs anything."""
|
|
565
|
+
import argparse
|
|
566
|
+
import contextlib
|
|
567
|
+
import io
|
|
568
|
+
import os
|
|
569
|
+
script = {"from all_users": ([("USERNAME",)], [("APP_AGENT",)]),
|
|
570
|
+
"from session_privs": ([("PRIVILEGE",)], [("DROP ANY TABLE",)])}
|
|
571
|
+
with tempfile.TemporaryDirectory() as td:
|
|
572
|
+
(pathlib.Path(td) / ".pythia").mkdir()
|
|
573
|
+
(pathlib.Path(td) / ".pythia" / "connections.json").write_text(
|
|
574
|
+
json.dumps({"dev": {"host": "h", "user": "APP", "password": "p",
|
|
575
|
+
"schema": "APP"}}), encoding="utf-8")
|
|
576
|
+
cwd = os.getcwd()
|
|
577
|
+
os.chdir(td)
|
|
578
|
+
try:
|
|
579
|
+
ns = argparse.Namespace(conn=None, json=True, save=False, name=None)
|
|
580
|
+
buf = io.StringIO()
|
|
581
|
+
with contextlib.redirect_stdout(buf):
|
|
582
|
+
pythia.cmd_agent_user(FakeConn(script), "APP", ns)
|
|
583
|
+
d = json.loads(buf.getvalue())
|
|
584
|
+
finally:
|
|
585
|
+
os.chdir(cwd)
|
|
586
|
+
assert d["agent_user_exists"] is True
|
|
587
|
+
assert "ALTER USER APP_AGENT IDENTIFIED BY" in d["sql"]
|
|
588
|
+
assert "ACCOUNT UNLOCK" in d["sql"]
|
|
589
|
+
assert "CREATE USER" not in d["sql"]
|
|
590
|
+
assert d["check_will_warn"] is True
|
|
591
|
+
assert d["owner_dangerous_privs"] == ["DROP ANY TABLE"]
|
|
592
|
+
|
|
593
|
+
|
|
492
594
|
def main():
|
|
493
595
|
failed = 0
|
|
494
596
|
for name, fn in sorted(globals().items()):
|
|
@@ -10,9 +10,9 @@ import sys
|
|
|
10
10
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
11
11
|
SKILLS = ROOT / "skills"
|
|
12
12
|
|
|
13
|
-
EXPECTED = {"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
EXPECTED = {"pythia-setup", "pythia-explore", "pythia-impact",
|
|
14
|
+
"pythia-write", "pythia-apply", "pythia-review",
|
|
15
|
+
"pythia-skill-author"}
|
|
16
16
|
|
|
17
17
|
# spec: SKILL.md under 150 lines, detail pushed to reference/
|
|
18
18
|
MAX_LINES = 150
|
|
@@ -65,19 +65,19 @@ def test_skill_bodies_stay_within_budget():
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def test_apply_skill_keeps_the_honest_rollback_table():
|
|
68
|
-
path = SKILLS / "
|
|
68
|
+
path = SKILLS / "pythia-apply" / "SKILL.md"
|
|
69
69
|
if not path.is_file():
|
|
70
70
|
return
|
|
71
71
|
text = path.read_text(encoding="utf-8")
|
|
72
72
|
for needle in ("Yes — completely", "Flashback Query", "Recycle Bin",
|
|
73
73
|
"Almost never", "exit 3", "run-sql"):
|
|
74
|
-
assert needle in text, f"
|
|
74
|
+
assert needle in text, f"pythia-apply: missing {needle!r}"
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def test_reference_files_exist_where_promised():
|
|
78
|
-
for rel in ("
|
|
79
|
-
"
|
|
80
|
-
"
|
|
78
|
+
for rel in ("pythia-explore/reference/data-dictionary.md",
|
|
79
|
+
"pythia-write/reference/patterns.md",
|
|
80
|
+
"pythia-review/reference/antipatterns.md"):
|
|
81
81
|
assert (SKILLS / rel).is_file(), f"missing {rel}"
|
|
82
82
|
|
|
83
83
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythia_plsql-0.1.3 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|