pythia-plsql 0.2.0__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.
Files changed (39) hide show
  1. {pythia_plsql-0.2.0/scripts/pythia_plsql.egg-info → pythia_plsql-0.2.2}/PKG-INFO +12 -6
  2. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/README.md +11 -5
  3. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/pyproject.toml +1 -1
  4. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia.py +60 -11
  5. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2/scripts/pythia_plsql.egg-info}/PKG-INFO +12 -6
  6. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-setup/SKILL.md +9 -2
  7. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/tests/test_install.py +32 -0
  8. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/tests/test_phase1.py +1 -2
  9. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/tests/test_phase3.py +33 -0
  10. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/LICENSE +0 -0
  11. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/compile-errors.sql +0 -0
  12. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/dependencies.sql +0 -0
  13. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/impact.sql +0 -0
  14. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/invalid-objects.sql +0 -0
  15. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/name-occupants.sql +0 -0
  16. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/object-source.sql +0 -0
  17. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/plscope-enabled.sql +0 -0
  18. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/plscope-statements.sql +0 -0
  19. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/plscope-usages.sql +0 -0
  20. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/session-privileges.sql +0 -0
  21. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/similar-candidates.sql +0 -0
  22. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/queries/source.sql +0 -0
  23. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
  24. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  25. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  26. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  27. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  28. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/setup.cfg +0 -0
  29. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-apply/SKILL.md +0 -0
  30. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-explore/SKILL.md +0 -0
  31. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-explore/reference/data-dictionary.md +0 -0
  32. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-impact/SKILL.md +0 -0
  33. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-review/SKILL.md +0 -0
  34. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-review/reference/antipatterns.md +0 -0
  35. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-skill-author/SKILL.md +0 -0
  36. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-write/SKILL.md +0 -0
  37. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/skills/pythia-write/reference/patterns.md +0 -0
  38. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/tests/test_phase2.py +0 -0
  39. {pythia_plsql-0.2.0 → pythia_plsql-0.2.2}/tests/test_phase5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.2.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
@@ -121,12 +121,18 @@ every agent read them first (`pythia conventions` shows both).
121
121
 
122
122
  **The account is the real security layer** — the policy file is an application-side
123
123
  fence. Give the agent its own revocable credential with proxy authentication
124
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
125
- `pythia agent-user` generates that SQL for your current connection — password
126
- included, `--save` drops the credential into `connections.json` — but it is
127
- optional convenience: running
124
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
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
128
134
  [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
129
- by hand works just as well.
135
+ works just as well.
130
136
  `pythia check` warns when the session runs with more power than the task needs.
131
137
 
132
138
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -107,12 +107,18 @@ every agent read them first (`pythia conventions` shows both).
107
107
 
108
108
  **The account is the real security layer** — the policy file is an application-side
109
109
  fence. Give the agent its own revocable credential with proxy authentication
110
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
111
- `pythia agent-user` generates that SQL for your current connection — password
112
- included, `--save` drops the credential into `connections.json` — but it is
113
- optional convenience: running
110
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
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
114
120
  [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
115
- by hand works just as well.
121
+ works just as well.
116
122
  `pythia check` warns when the session runs with more power than the task needs.
117
123
 
118
124
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythia-plsql"
7
- version = "0.2.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" }
@@ -1355,7 +1355,7 @@ AGENT_USER_SQL = """\
1355
1355
  -- privileges, utility grants — the agent needs none of them to develop
1356
1356
  -- PL/SQL, and every extra grant widens the blast radius.
1357
1357
 
1358
- CREATE USER {agent} IDENTIFIED BY "{password}";
1358
+ {create_line}
1359
1359
  GRANT CREATE SESSION TO {agent};
1360
1360
  ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1361
1361
 
@@ -1365,6 +1365,23 @@ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1365
1365
  """
1366
1366
 
1367
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
+
1368
1385
  def agent_password():
1369
1386
  """Random password satisfying common Oracle verify functions: letter
1370
1387
  first, upper+lower+digit+special, 16+ chars. Always quoted in SQL."""
@@ -1403,18 +1420,51 @@ def cmd_agent_user(conn, schema, ns):
1403
1420
  f"({base['user']}) — nothing to set up.")
1404
1421
  agent = (ns.name or f"{owner}_AGENT").upper()
1405
1422
  password = agent_password()
1406
- print(AGENT_USER_SQL.format(owner=owner, agent=agent, password=password))
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
1407
1432
  if ns.save:
1408
- name = save_agent_connection(root, base_name, base, agent, password)
1409
- print(f"Saved connection '{name}' (now the default) in "
1410
- f"{pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME};\n"
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"
1411
1460
  f"the owner entry '{base_name}' is untouched — switch back "
1412
1461
  f"any time with --conn {base_name}.")
1413
- print(f"\nNext: run the SQL above as a DBA, then: {invocation()} check")
1462
+ print(f"\nNext: relay the SQL above to the developer verbatim for a "
1463
+ f"DBA to run, then: {invocation()} check")
1414
1464
  else:
1415
- print(f"-- Rerun with --save to add this credential to "
1416
- f"connections.json as '{base_name}_agent' and make it the "
1417
- f"default once the DBA has run it.")
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.")
1418
1468
 
1419
1469
 
1420
1470
  # unistr escapes. Single quote doubles to '' per SQL (a \' is ORA-01756 for
@@ -1559,8 +1609,7 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
1559
1609
  "conventions": cmd_conventions, "install": cmd_install,
1560
1610
  "unistr": cmd_unistr, "agent-user": cmd_agent_user}
1561
1611
 
1562
- NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr",
1563
- "agent-user"}
1612
+ NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"}
1564
1613
 
1565
1614
 
1566
1615
  # --- CLI ---------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.2.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
@@ -121,12 +121,18 @@ every agent read them first (`pythia conventions` shows both).
121
121
 
122
122
  **The account is the real security layer** — the policy file is an application-side
123
123
  fence. Give the agent its own revocable credential with proxy authentication
124
- (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared).
125
- `pythia agent-user` generates that SQL for your current connection — password
126
- included, `--save` drops the credential into `connections.json` — but it is
127
- optional convenience: running
124
+ (`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
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
128
134
  [`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
129
- by hand works just as well.
135
+ works just as well.
130
136
  `pythia check` warns when the session runs with more power than the task needs.
131
137
 
132
138
  Per-group write policy, `.pythia/policy.json` (defaults shown):
@@ -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 `examples/agent-user-setup.example.sql` (as a DBA, names adapted), then
45
- set the connection's user to `"agent_user[schema_owner]"`.
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.
@@ -87,6 +87,38 @@ def test_pack_dirs_resolve_in_the_source_layout():
87
87
  assert (pythia.SKILLS_DIR / "pythia-apply" / "SKILL.md").is_file()
88
88
 
89
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)
120
+
121
+
90
122
  def test_one_version_across_every_manifest():
91
123
  """pyproject, plugin.json, marketplace.json and npm/package.json must all
92
124
  carry the same version — the release workflow publishes from one tag."""
@@ -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.AGENT_USER_SQL.format(owner="APP", agent="APP_AGENT",
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
@@ -558,6 +558,39 @@ def test_journal_prune_keeps_applied_entries():
558
558
  assert pythia.list_journal_entries(td) == [kept]
559
559
 
560
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
+
561
594
  def main():
562
595
  failed = 0
563
596
  for name, fn in sorted(globals().items()):
File without changes
File without changes