pythia-plsql 0.2.0__tar.gz → 0.3.0__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.2.0/scripts/pythia_plsql.egg-info → pythia_plsql-0.3.0}/PKG-INFO +26 -10
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/README.md +25 -9
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/pyproject.toml +1 -1
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia.py +188 -35
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0/scripts/pythia_plsql.egg-info}/PKG-INFO +26 -10
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-apply/SKILL.md +10 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-setup/SKILL.md +9 -2
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-write/SKILL.md +8 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/tests/test_install.py +65 -30
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/tests/test_phase1.py +1 -2
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/tests/test_phase3.py +100 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/LICENSE +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/compile-errors.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/dependencies.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/impact.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/invalid-objects.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/name-occupants.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/object-source.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/plscope-enabled.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/plscope-statements.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/plscope-usages.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/session-privileges.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/similar-candidates.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/queries/source.sql +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/setup.cfg +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-explore/SKILL.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-explore/reference/data-dictionary.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-impact/SKILL.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-review/SKILL.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-review/reference/antipatterns.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-skill-author/SKILL.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-write/reference/patterns.md +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/tests/test_phase2.py +0 -0
- {pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/tests/test_phase5.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -20,7 +20,7 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
> Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
|
|
22
22
|
|
|
23
|
-
**English** · [Tiếng Việt](README.vi.md)
|
|
23
|
+
**English** · [Tiếng Việt](README.vi.md) · **[The Complete Guide →](GUIDE.md)**
|
|
24
24
|
|
|
25
25
|
[](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
|
|
26
26
|
[](LICENSE)
|
|
@@ -81,9 +81,10 @@ pythia check # fill in connections.json first, then verify
|
|
|
81
81
|
|
|
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
|
-
for internal mirrors) — without Node it copies the bundled
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
for internal mirrors) — without Node it copies the bundled pack itself.
|
|
85
|
+
Prefer **global skills**: `pythia install -g` once per machine serves every
|
|
86
|
+
project, and per-project installs then skip the skills step automatically —
|
|
87
|
+
one copy anywhere means no skill ever shows up twice in the agent's menu. Skills alone: `npx skills add
|
|
87
88
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
88
89
|
|
|
89
90
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -92,6 +93,15 @@ project** — run it in each repo's root to drop the skills and a fresh
|
|
|
92
93
|
project you are standing in (searched upward from the current directory,
|
|
93
94
|
no global fallback), so one global CLI never mixes up databases.
|
|
94
95
|
|
|
96
|
+
**Updating** follows the same split:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install --upgrade pythia-plsql # new CLI version (once per machine)
|
|
100
|
+
pythia install # refresh a project's skills; config never touched
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
(`npx skills update` also works for skills installed via npx.)
|
|
104
|
+
|
|
95
105
|
Running from a clone works too — `python scripts/pythia.py <command>`; every
|
|
96
106
|
printed follow-up command matches how you invoked it. Windows, macOS, Linux
|
|
97
107
|
and WSL are all CI-tested.
|
|
@@ -121,12 +131,18 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
121
131
|
|
|
122
132
|
**The account is the real security layer** — the policy file is an application-side
|
|
123
133
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
124
|
-
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
138
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
142
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
143
|
+
hand with
|
|
128
144
|
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
129
|
-
|
|
145
|
+
works just as well.
|
|
130
146
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
131
147
|
|
|
132
148
|
Per-group write policy, `.pythia/policy.json` (defaults shown):
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
|
|
8
8
|
|
|
9
|
-
**English** · [Tiếng Việt](README.vi.md)
|
|
9
|
+
**English** · [Tiếng Việt](README.vi.md) · **[The Complete Guide →](GUIDE.md)**
|
|
10
10
|
|
|
11
11
|
[](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
|
|
12
12
|
[](LICENSE)
|
|
@@ -67,9 +67,10 @@ pythia check # fill in connections.json first, then verify
|
|
|
67
67
|
|
|
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
|
-
for internal mirrors) — without Node it copies the bundled
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
for internal mirrors) — without Node it copies the bundled pack itself.
|
|
71
|
+
Prefer **global skills**: `pythia install -g` once per machine serves every
|
|
72
|
+
project, and per-project installs then skip the skills step automatically —
|
|
73
|
+
one copy anywhere means no skill ever shows up twice in the agent's menu. Skills alone: `npx skills add
|
|
73
74
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
74
75
|
|
|
75
76
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -78,6 +79,15 @@ project** — run it in each repo's root to drop the skills and a fresh
|
|
|
78
79
|
project you are standing in (searched upward from the current directory,
|
|
79
80
|
no global fallback), so one global CLI never mixes up databases.
|
|
80
81
|
|
|
82
|
+
**Updating** follows the same split:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install --upgrade pythia-plsql # new CLI version (once per machine)
|
|
86
|
+
pythia install # refresh a project's skills; config never touched
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
(`npx skills update` also works for skills installed via npx.)
|
|
90
|
+
|
|
81
91
|
Running from a clone works too — `python scripts/pythia.py <command>`; every
|
|
82
92
|
printed follow-up command matches how you invoked it. Windows, macOS, Linux
|
|
83
93
|
and WSL are all CI-tested.
|
|
@@ -107,12 +117,18 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
107
117
|
|
|
108
118
|
**The account is the real security layer** — the policy file is an application-side
|
|
109
119
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
110
|
-
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
124
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
128
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
129
|
+
hand with
|
|
114
130
|
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
115
|
-
|
|
131
|
+
works just as well.
|
|
116
132
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
117
133
|
|
|
118
134
|
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.
|
|
7
|
+
version = "0.3.0"
|
|
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" }
|
|
@@ -201,6 +201,19 @@ def invocation():
|
|
|
201
201
|
return pathlib.Path(prog).name
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
def human_at_the_keyboard():
|
|
205
|
+
"""True only when a person is typing: stdin is a TTY. An agent driving
|
|
206
|
+
the CLI through a subprocess has no TTY and cannot fake one — which is
|
|
207
|
+
exactly why the loosening actions below key on this. PYTHIA_CI=1 is the
|
|
208
|
+
documented escape for real pipelines."""
|
|
209
|
+
if os.environ.get("PYTHIA_CI"):
|
|
210
|
+
return True
|
|
211
|
+
try:
|
|
212
|
+
return sys.stdin.isatty()
|
|
213
|
+
except (AttributeError, ValueError):
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
204
217
|
def forbid_write_flag(argv):
|
|
205
218
|
if "--write" in argv:
|
|
206
219
|
sys.exit(f"There is no --write flag. The write path is `{invocation()} "
|
|
@@ -1110,6 +1123,12 @@ def run_apply(conn, schema, ns, file_text, origin=None):
|
|
|
1110
1123
|
{"s": schema, "n": name, "t": otype})
|
|
1111
1124
|
db_source = "".join(cell(r[0]) for r in rows)
|
|
1112
1125
|
token = apply_token(otype, name, file_text, db_source)
|
|
1126
|
+
if ns.yes and not human_at_the_keyboard():
|
|
1127
|
+
sys.exit("--yes is the developer's flag, and no terminal is attached "
|
|
1128
|
+
"to this session.\nAgents preview, relay the diff and the "
|
|
1129
|
+
"impact verbatim, STOP, and pass --confirm <token> only "
|
|
1130
|
+
"after the developer's explicit approval in chat.\n"
|
|
1131
|
+
"(Real pipelines set PYTHIA_CI=1.)")
|
|
1113
1132
|
confirmed = bool(ns.yes) or ns.confirm == token
|
|
1114
1133
|
if ns.confirm and ns.confirm != token:
|
|
1115
1134
|
sys.exit("The confirmation token does not match: the file or the "
|
|
@@ -1120,6 +1139,9 @@ def run_apply(conn, schema, ns, file_text, origin=None):
|
|
|
1120
1139
|
invalid_before = [(r[0], r[1]) for r in inv_rows]
|
|
1121
1140
|
meta = {"schema": schema, "connection": ns.conn_name, "group": group,
|
|
1122
1141
|
"token": token, "applied": False,
|
|
1142
|
+
"confirmed_via": ("yes" if ns.yes else
|
|
1143
|
+
"token" if confirmed else "preview"),
|
|
1144
|
+
"tty": human_at_the_keyboard(),
|
|
1123
1145
|
"invalid_before": invalid_before, **(origin or {})}
|
|
1124
1146
|
entry = write_journal_entry(ns.project_root, otype, name, db_source,
|
|
1125
1147
|
file_text, meta)
|
|
@@ -1263,6 +1285,16 @@ def cmd_policy(conn, schema, ns):
|
|
|
1263
1285
|
"values: allow, confirm, deny.")
|
|
1264
1286
|
if ns.action == "set":
|
|
1265
1287
|
eff = load_policy(ns.project_root)
|
|
1288
|
+
RANK = {"deny": 0, "confirm": 1, "allow": 2}
|
|
1289
|
+
if (RANK[ns.value] > RANK[eff[ns.group][0]]
|
|
1290
|
+
and not human_at_the_keyboard()):
|
|
1291
|
+
sys.exit(f"Loosening the write policy ({ns.group}: "
|
|
1292
|
+
f"{eff[ns.group][0]} → {ns.value}) is the developer's "
|
|
1293
|
+
"decision, and no terminal is attached to this session."
|
|
1294
|
+
"\nAsk the developer to run this themselves:\n"
|
|
1295
|
+
f" {invocation()} policy set {ns.group} {ns.value}\n"
|
|
1296
|
+
"(Tightening is always allowed; pipelines set "
|
|
1297
|
+
"PYTHIA_CI=1.)")
|
|
1266
1298
|
eff[ns.group] = (ns.value, "policy.json") # validated by argparse choices
|
|
1267
1299
|
path = policy_path(ns.project_root)
|
|
1268
1300
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1355,7 +1387,7 @@ AGENT_USER_SQL = """\
|
|
|
1355
1387
|
-- privileges, utility grants — the agent needs none of them to develop
|
|
1356
1388
|
-- PL/SQL, and every extra grant widens the blast radius.
|
|
1357
1389
|
|
|
1358
|
-
|
|
1390
|
+
{create_line}
|
|
1359
1391
|
GRANT CREATE SESSION TO {agent};
|
|
1360
1392
|
ALTER USER {owner} GRANT CONNECT THROUGH {agent};
|
|
1361
1393
|
|
|
@@ -1365,6 +1397,23 @@ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
|
|
|
1365
1397
|
"""
|
|
1366
1398
|
|
|
1367
1399
|
|
|
1400
|
+
def agent_user_sql(owner, agent, password, exists):
|
|
1401
|
+
"""exists True: the user is already there — CREATE would be ORA-01920,
|
|
1402
|
+
so reset the password and unlock in one statement (harmless if it was
|
|
1403
|
+
never locked). exists None: no database to ask — emit CREATE plus the
|
|
1404
|
+
fallback as a comment so the DBA can pick."""
|
|
1405
|
+
if exists:
|
|
1406
|
+
line = (f'ALTER USER {agent} IDENTIFIED BY "{password}" ACCOUNT '
|
|
1407
|
+
f"UNLOCK; -- user already exists; CREATE would be ORA-01920")
|
|
1408
|
+
else:
|
|
1409
|
+
line = f'CREATE USER {agent} IDENTIFIED BY "{password}";'
|
|
1410
|
+
if exists is None:
|
|
1411
|
+
line += ("\n-- (if the user already exists — ORA-01920 — run "
|
|
1412
|
+
"instead:\n-- ALTER USER "
|
|
1413
|
+
f'{agent} IDENTIFIED BY "{password}" ACCOUNT UNLOCK;)')
|
|
1414
|
+
return AGENT_USER_SQL.format(owner=owner, agent=agent, create_line=line)
|
|
1415
|
+
|
|
1416
|
+
|
|
1368
1417
|
def agent_password():
|
|
1369
1418
|
"""Random password satisfying common Oracle verify functions: letter
|
|
1370
1419
|
first, upper+lower+digit+special, 16+ chars. Always quoted in SQL."""
|
|
@@ -1403,18 +1452,51 @@ def cmd_agent_user(conn, schema, ns):
|
|
|
1403
1452
|
f"({base['user']}) — nothing to set up.")
|
|
1404
1453
|
agent = (ns.name or f"{owner}_AGENT").upper()
|
|
1405
1454
|
password = agent_password()
|
|
1406
|
-
|
|
1455
|
+
exists, dangerous = None, None
|
|
1456
|
+
if conn is not None:
|
|
1457
|
+
_, rows = run_query(conn, "select username from all_users "
|
|
1458
|
+
"where username = :n", {"n": agent})
|
|
1459
|
+
exists = bool(rows)
|
|
1460
|
+
_, prows = run_query(conn, load_query("session-privileges.sql"))
|
|
1461
|
+
dangerous = [r[0] for r in prows]
|
|
1462
|
+
sql = agent_user_sql(owner, agent, password, exists)
|
|
1463
|
+
saved = None
|
|
1407
1464
|
if ns.save:
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1465
|
+
saved = save_agent_connection(root, base_name, base, agent, password)
|
|
1466
|
+
if ns.json:
|
|
1467
|
+
print(json.dumps({
|
|
1468
|
+
"owner": owner, "agent": agent, "password": password, "sql": sql,
|
|
1469
|
+
"agent_user_exists": exists, "saved_connection": saved,
|
|
1470
|
+
"owner_connection": base_name,
|
|
1471
|
+
"check_will_warn": bool(dangerous) if dangerous is not None else None,
|
|
1472
|
+
"owner_dangerous_privs": dangerous,
|
|
1473
|
+
"next": ["have a DBA run the sql", f"{invocation()} check"]}))
|
|
1474
|
+
return
|
|
1475
|
+
print(sql)
|
|
1476
|
+
print("-- The password is regenerated on EVERY run — only the SQL from")
|
|
1477
|
+
print("-- this exact run matches what --save writes. One run, not two.")
|
|
1478
|
+
if dangerous:
|
|
1479
|
+
print(f"\n! Even after this, {invocation()} check will STILL warn: "
|
|
1480
|
+
f"the owner {owner} holds\n {', '.join(dangerous[:3])}"
|
|
1481
|
+
+ ("…" if len(dangerous) > 3 else "")
|
|
1482
|
+
+ " — every proxy session inherits it.\n"
|
|
1483
|
+
" Trim the owner's grants to reach the no-warning goal state "
|
|
1484
|
+
"(see README, Security).")
|
|
1485
|
+
elif dangerous is not None:
|
|
1486
|
+
print(f"\nOwner grants look clean: after the DBA runs this, "
|
|
1487
|
+
f"{invocation()} check should pass with no warning.")
|
|
1488
|
+
if saved:
|
|
1489
|
+
print(f"\nSaved connection '{saved}' (now the default) in "
|
|
1490
|
+
f"{pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME} — its "
|
|
1491
|
+
f"password matches the SQL above;\n"
|
|
1411
1492
|
f"the owner entry '{base_name}' is untouched — switch back "
|
|
1412
1493
|
f"any time with --conn {base_name}.")
|
|
1413
|
-
print(f"\nNext:
|
|
1494
|
+
print(f"\nNext: relay the SQL above to the developer verbatim for a "
|
|
1495
|
+
f"DBA to run, then: {invocation()} check")
|
|
1414
1496
|
else:
|
|
1415
|
-
print(f"--
|
|
1416
|
-
f"connections.json as '{base_name}_agent'
|
|
1417
|
-
f"
|
|
1497
|
+
print(f"-- Nothing was saved. Use --save to also write this "
|
|
1498
|
+
f"credential to connections.json as '{base_name}_agent' in "
|
|
1499
|
+
f"the same run.")
|
|
1418
1500
|
|
|
1419
1501
|
|
|
1420
1502
|
# unistr escapes. Single quote doubles to '' per SQL (a \' is ORA-01756 for
|
|
@@ -1489,7 +1571,7 @@ def scaffold_config(root):
|
|
|
1489
1571
|
return path, True
|
|
1490
1572
|
|
|
1491
1573
|
|
|
1492
|
-
def run_skills_add(source, interactive=False):
|
|
1574
|
+
def run_skills_add(source, interactive=False, glob=False):
|
|
1493
1575
|
"""Install the skill pack via the skills CLI. Returns its exit code, or
|
|
1494
1576
|
None when npx is absent — the caller then copies the bundled pack, so a
|
|
1495
1577
|
machine with only Python still gets the whole kit. At a TTY the CLI's own
|
|
@@ -1499,10 +1581,22 @@ def run_skills_add(source, interactive=False):
|
|
|
1499
1581
|
npx = shutil.which("npx")
|
|
1500
1582
|
if npx is None:
|
|
1501
1583
|
return None
|
|
1584
|
+
return subprocess.run(skills_add_cmd(npx, source, interactive,
|
|
1585
|
+
glob=glob)).returncode
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
def skills_add_cmd(npx, source, interactive, glob=False):
|
|
1589
|
+
"""Interactive: the CLI's own agent picker. Non-interactive project
|
|
1590
|
+
installs pin one agent target so nothing is double-installed; global
|
|
1591
|
+
installs let the CLI place per-agent copies under the home directory."""
|
|
1502
1592
|
cmd = [npx, "skills", "add", source]
|
|
1593
|
+
if glob:
|
|
1594
|
+
cmd.append("-g")
|
|
1503
1595
|
if not interactive:
|
|
1504
1596
|
cmd.append("-y")
|
|
1505
|
-
|
|
1597
|
+
if not glob:
|
|
1598
|
+
cmd += ["-a", "claude-code"]
|
|
1599
|
+
return cmd
|
|
1506
1600
|
|
|
1507
1601
|
|
|
1508
1602
|
LEGACY_SKILLS = ("plsql-setup", "plsql-explore", "plsql-impact",
|
|
@@ -1510,43 +1604,100 @@ LEGACY_SKILLS = ("plsql-setup", "plsql-explore", "plsql-impact",
|
|
|
1510
1604
|
"plsql-skill-author")
|
|
1511
1605
|
|
|
1512
1606
|
|
|
1513
|
-
def
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1607
|
+
def pack_names():
|
|
1608
|
+
return [p.name for p in sorted(SKILLS_DIR.iterdir())
|
|
1609
|
+
if (p / "SKILL.md").is_file()]
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def global_pack_present(home=None):
|
|
1613
|
+
"""Is the skill pack already installed machine-wide? Then a project copy
|
|
1614
|
+
would only duplicate every entry in the agent's menu."""
|
|
1615
|
+
home = pathlib.Path(home) if home else pathlib.Path.home()
|
|
1616
|
+
return any((home / rel / "pythia-apply" / "SKILL.md").is_file()
|
|
1617
|
+
for rel in (".claude/skills", ".agents/skills",
|
|
1618
|
+
".config/agents/skills"))
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def copy_bundled_skills(base_dir):
|
|
1622
|
+
"""No-Node fallback: copy the wheel-bundled pack into
|
|
1623
|
+
<base>/.claude/skills/ ONLY — the directory Claude Code reliably reads
|
|
1624
|
+
in both scopes (field evidence: project .agents/skills is invisible to
|
|
1625
|
+
some Claude Code versions, and a second copy doubles every menu entry).
|
|
1626
|
+
base_dir is the project root, or the home directory for -g. The copy
|
|
1627
|
+
merges: only the pack's own skills are refreshed; stale copies of the
|
|
1628
|
+
pack under .agents/skills and old plsql-* names are cleaned."""
|
|
1521
1629
|
import shutil
|
|
1522
|
-
dest_root = pathlib.Path(
|
|
1630
|
+
dest_root = pathlib.Path(base_dir) / ".claude" / "skills"
|
|
1523
1631
|
for pack in sorted(SKILLS_DIR.iterdir()):
|
|
1524
1632
|
if not (pack / "SKILL.md").is_file():
|
|
1525
1633
|
continue
|
|
1526
1634
|
shutil.copytree(pack, dest_root / pack.name, dirs_exist_ok=True)
|
|
1635
|
+
clean_legacy_skills(base_dir)
|
|
1636
|
+
for name in pack_names(): # canonical copy lives in .claude now
|
|
1637
|
+
_remove_link_first(pathlib.Path(base_dir) / ".agents" / "skills" / name)
|
|
1638
|
+
return [dest_root]
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def _remove_link_first(path):
|
|
1642
|
+
"""Windows junctions defeat is_symlink(); unlink/rmdir take out a file,
|
|
1643
|
+
symlink, junction or empty dir without touching any target — only a
|
|
1644
|
+
real populated directory needs rmtree."""
|
|
1645
|
+
import shutil
|
|
1646
|
+
if not os.path.lexists(path):
|
|
1647
|
+
return
|
|
1648
|
+
try:
|
|
1649
|
+
path.unlink()
|
|
1650
|
+
except OSError:
|
|
1651
|
+
try:
|
|
1652
|
+
os.rmdir(path)
|
|
1653
|
+
except OSError:
|
|
1654
|
+
shutil.rmtree(path)
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
def clean_legacy_skills(base_dir):
|
|
1658
|
+
"""Remove stale copies under the pack's old plsql-* names from both
|
|
1659
|
+
conventional roots below base_dir (project root or home)."""
|
|
1527
1660
|
for rel in (".agents/skills", ".claude/skills"):
|
|
1528
1661
|
for old_name in LEGACY_SKILLS:
|
|
1529
|
-
|
|
1530
|
-
if legacy.is_dir() and not legacy.is_symlink():
|
|
1531
|
-
shutil.rmtree(legacy)
|
|
1532
|
-
return [dest_root]
|
|
1662
|
+
_remove_link_first(pathlib.Path(base_dir) / rel / old_name)
|
|
1533
1663
|
|
|
1534
1664
|
|
|
1535
1665
|
def cmd_install(conn, schema, ns):
|
|
1536
1666
|
en = getattr(ns, "color", False)
|
|
1537
1667
|
if en:
|
|
1538
1668
|
sys.stdout.write(banner(en))
|
|
1669
|
+
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
1670
|
+
if ns.glob:
|
|
1671
|
+
code = run_skills_add(ns.source, interactive, glob=True)
|
|
1672
|
+
if code is None:
|
|
1673
|
+
for tgt in copy_bundled_skills(pathlib.Path.home()):
|
|
1674
|
+
print(f"Copied the bundled skill pack into {tgt}")
|
|
1675
|
+
elif code:
|
|
1676
|
+
sys.exit(code)
|
|
1677
|
+
else:
|
|
1678
|
+
clean_legacy_skills(pathlib.Path.home())
|
|
1679
|
+
print("\nGlobal skills serve every project — per-project installs "
|
|
1680
|
+
"will now skip the skills step automatically.")
|
|
1681
|
+
return
|
|
1539
1682
|
path, created = scaffold_config(ns.project_root)
|
|
1540
1683
|
print(f"{'Created' if created else 'Kept existing'} {path}")
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1684
|
+
if global_pack_present():
|
|
1685
|
+
print("Skill pack found in the global install — it already serves "
|
|
1686
|
+
"this project,\nso no project copy is made (a second copy "
|
|
1687
|
+
f"doubles every menu entry).\nRefresh it with: {invocation()} "
|
|
1688
|
+
"install -g")
|
|
1689
|
+
clean_legacy_skills(ns.project_root)
|
|
1690
|
+
else:
|
|
1691
|
+
code = run_skills_add(ns.source, interactive)
|
|
1692
|
+
if code is None:
|
|
1693
|
+
for tgt in copy_bundled_skills(ns.project_root):
|
|
1694
|
+
print(f"Copied the bundled skill pack into {tgt}")
|
|
1695
|
+
print("(npx not found — with Node.js, `npx skills add` reaches "
|
|
1696
|
+
"77 agents with symlinked updates.)")
|
|
1697
|
+
elif code:
|
|
1698
|
+
sys.exit(code)
|
|
1699
|
+
else:
|
|
1700
|
+
clean_legacy_skills(ns.project_root)
|
|
1550
1701
|
print(f"\nNext: fill in {path}")
|
|
1551
1702
|
print(f"Then: {invocation()} check")
|
|
1552
1703
|
|
|
@@ -1559,8 +1710,7 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
|
|
|
1559
1710
|
"conventions": cmd_conventions, "install": cmd_install,
|
|
1560
1711
|
"unistr": cmd_unistr, "agent-user": cmd_agent_user}
|
|
1561
1712
|
|
|
1562
|
-
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"
|
|
1563
|
-
"agent-user"}
|
|
1713
|
+
NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr"}
|
|
1564
1714
|
|
|
1565
1715
|
|
|
1566
1716
|
# --- CLI ---------------------------------------------------------------------
|
|
@@ -1674,6 +1824,9 @@ def build_parser():
|
|
|
1674
1824
|
help="wrap as 'loi:'||unistr(...)||':loi'")
|
|
1675
1825
|
s = sub.add_parser("install", parents=[common()],
|
|
1676
1826
|
help="install the skill pack and scaffold .pythia/ config")
|
|
1827
|
+
s.add_argument("-g", "--global", dest="glob", action="store_true",
|
|
1828
|
+
help="install the skill pack machine-wide (serves every "
|
|
1829
|
+
"project; per-project installs then skip skills)")
|
|
1677
1830
|
s.add_argument("--source", default=DEFAULT_SKILLS_SOURCE,
|
|
1678
1831
|
help="skills repo for `npx skills add` "
|
|
1679
1832
|
f"(default {DEFAULT_SKILLS_SOURCE})")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythia-plsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -20,7 +20,7 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
> Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
|
|
22
22
|
|
|
23
|
-
**English** · [Tiếng Việt](README.vi.md)
|
|
23
|
+
**English** · [Tiếng Việt](README.vi.md) · **[The Complete Guide →](GUIDE.md)**
|
|
24
24
|
|
|
25
25
|
[](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
|
|
26
26
|
[](LICENSE)
|
|
@@ -81,9 +81,10 @@ pythia check # fill in connections.json first, then verify
|
|
|
81
81
|
|
|
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
|
-
for internal mirrors) — without Node it copies the bundled
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
for internal mirrors) — without Node it copies the bundled pack itself.
|
|
85
|
+
Prefer **global skills**: `pythia install -g` once per machine serves every
|
|
86
|
+
project, and per-project installs then skip the skills step automatically —
|
|
87
|
+
one copy anywhere means no skill ever shows up twice in the agent's menu. Skills alone: `npx skills add
|
|
87
88
|
thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
|
|
88
89
|
|
|
89
90
|
`pip install` is **once per machine**; `pythia install` is **once per
|
|
@@ -92,6 +93,15 @@ project** — run it in each repo's root to drop the skills and a fresh
|
|
|
92
93
|
project you are standing in (searched upward from the current directory,
|
|
93
94
|
no global fallback), so one global CLI never mixes up databases.
|
|
94
95
|
|
|
96
|
+
**Updating** follows the same split:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install --upgrade pythia-plsql # new CLI version (once per machine)
|
|
100
|
+
pythia install # refresh a project's skills; config never touched
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
(`npx skills update` also works for skills installed via npx.)
|
|
104
|
+
|
|
95
105
|
Running from a clone works too — `python scripts/pythia.py <command>`; every
|
|
96
106
|
printed follow-up command matches how you invoked it. Windows, macOS, Linux
|
|
97
107
|
and WSL are all CI-tested.
|
|
@@ -121,12 +131,18 @@ every agent read them first (`pythia conventions` shows both).
|
|
|
121
131
|
|
|
122
132
|
**The account is the real security layer** — the policy file is an application-side
|
|
123
133
|
fence. Give the agent its own revocable credential with proxy authentication
|
|
124
|
-
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
(`agent_user[schema_owner]`, no `ANY` privileges, no owner password shared):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pythia agent-user --save # SQL for the DBA + matching credential saved as <conn>_agent
|
|
138
|
+
pythia check # after the DBA ran it: proxy session, warning gone
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
One run does both — the password is regenerated each run, so the SQL and the
|
|
142
|
+
saved config must come from the same run. Optional convenience: doing it by
|
|
143
|
+
hand with
|
|
128
144
|
[`examples/agent-user-setup.example.sql`](examples/agent-user-setup.example.sql)
|
|
129
|
-
|
|
145
|
+
works just as well.
|
|
130
146
|
`pythia check` warns when the session runs with more power than the task needs.
|
|
131
147
|
|
|
132
148
|
Per-group write policy, `.pythia/policy.json` (defaults shown):
|
|
@@ -21,6 +21,16 @@ Applies to every write: new objects, fixes, restores, batch runs. No
|
|
|
21
21
|
exceptions for "trivial" changes — a one-line change can invalidate twelve
|
|
22
22
|
dependents.
|
|
23
23
|
|
|
24
|
+
Three moves are FORBIDDEN for agents, and the CLI enforces the first two
|
|
25
|
+
(no terminal attached → refusal):
|
|
26
|
+
|
|
27
|
+
- `--yes` — it is the developer's flag, never yours.
|
|
28
|
+
- `pythia policy set <group>` to anything LOOSER — loosening policy is the
|
|
29
|
+
developer's decision; hand them the exact command to run themselves.
|
|
30
|
+
- Passing `--confirm` in the same turn as the preview. The preview ends
|
|
31
|
+
your turn; the token is only used after the developer's approval
|
|
32
|
+
arrives as a NEW message.
|
|
33
|
+
|
|
24
34
|
## The Workflow
|
|
25
35
|
|
|
26
36
|
**Before step 1:** if this conversation has not yet seen a standalone impact
|
|
@@ -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.
|
|
@@ -40,6 +40,14 @@ existing ones — not to introduce a better style.
|
|
|
40
40
|
`reference/patterns.md` — cursor and bulk patterns, exception discipline,
|
|
41
41
|
bind variables, commit ownership.
|
|
42
42
|
|
|
43
|
+
## Scope is the developer's sentence, not yours
|
|
44
|
+
|
|
45
|
+
Change ONLY the objects the developer named. When impact shows dependents
|
|
46
|
+
that will break, you REPORT them with the list and a proposal — fixing
|
|
47
|
+
them is a separate request that needs its own explicit approval. "While I
|
|
48
|
+
was there" is how an asked-for table edit becomes seven unasked procedure
|
|
49
|
+
rewrites.
|
|
50
|
+
|
|
43
51
|
## Conventions outrank preferences
|
|
44
52
|
|
|
45
53
|
If the codebase writes explicit cursors where you would write `FOR r IN`,
|
|
@@ -35,11 +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
|
-
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
pack
|
|
38
|
+
"""No Node -> the whole kit still installs, into .claude/skills ONLY —
|
|
39
|
+
the directory Claude Code reliably reads (field evidence: project
|
|
40
|
+
.agents/skills is invisible to some versions, and any second copy
|
|
41
|
+
doubles every menu entry). Foreign skills survive; stale plsql-* and
|
|
42
|
+
old .agents pack copies are cleaned."""
|
|
43
43
|
import shutil
|
|
44
44
|
old = shutil.which
|
|
45
45
|
shutil.which = lambda name: None
|
|
@@ -50,36 +50,39 @@ def test_missing_npx_falls_back_to_the_bundled_pack():
|
|
|
50
50
|
with tempfile.TemporaryDirectory() as td:
|
|
51
51
|
agents = pathlib.Path(td) / ".agents" / "skills"
|
|
52
52
|
claude = pathlib.Path(td) / ".claude" / "skills"
|
|
53
|
-
# pre-existing content: a custom skill, and stale old-name copies
|
|
54
53
|
(agents / "my-team-skill").mkdir(parents=True)
|
|
55
54
|
(agents / "my-team-skill" / "SKILL.md").write_text("x", encoding="utf-8")
|
|
56
|
-
(agents / "
|
|
55
|
+
(agents / "pythia-apply").mkdir() # stale copy from 0.2.0-0.2.3
|
|
57
56
|
(claude / "plsql-review").mkdir(parents=True)
|
|
58
57
|
targets = pythia.copy_bundled_skills(td)
|
|
59
|
-
assert targets == [
|
|
60
|
-
assert (
|
|
61
|
-
assert (
|
|
62
|
-
assert not (
|
|
58
|
+
assert targets == [claude]
|
|
59
|
+
assert (claude / "pythia-apply" / "SKILL.md").is_file()
|
|
60
|
+
assert (claude / "pythia-review" / "reference" / "antipatterns.md").is_file()
|
|
61
|
+
assert not (agents / "pythia-apply").exists() # single copy
|
|
63
62
|
assert (agents / "my-team-skill" / "SKILL.md").read_text(
|
|
64
|
-
encoding="utf-8") == "x"
|
|
65
|
-
assert not (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
encoding="utf-8") == "x" # merge, not wipe
|
|
64
|
+
assert not (claude / "plsql-review").exists() # legacy cleaned
|
|
65
|
+
pythia.copy_bundled_skills(td) # idempotent
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_global_pack_detection_skips_project_copies():
|
|
69
|
+
with tempfile.TemporaryDirectory() as home:
|
|
70
|
+
assert not pythia.global_pack_present(home)
|
|
71
|
+
d = pathlib.Path(home) / ".claude" / "skills" / "pythia-apply"
|
|
72
|
+
d.mkdir(parents=True)
|
|
73
|
+
(d / "SKILL.md").write_text("x", encoding="utf-8")
|
|
74
|
+
assert pythia.global_pack_present(home)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_skills_add_cmd_scopes():
|
|
78
|
+
"""Unattended project installs pin one agent (no double-install);
|
|
79
|
+
global installs use -g; the interactive picker stays untouched."""
|
|
80
|
+
assert pythia.skills_add_cmd("npx", "o/r", interactive=False) == \
|
|
81
|
+
["npx", "skills", "add", "o/r", "-y", "-a", "claude-code"]
|
|
82
|
+
assert pythia.skills_add_cmd("npx", "o/r", interactive=False, glob=True) == \
|
|
83
|
+
["npx", "skills", "add", "o/r", "-g", "-y"]
|
|
84
|
+
assert pythia.skills_add_cmd("npx", "o/r", interactive=True) == \
|
|
85
|
+
["npx", "skills", "add", "o/r"]
|
|
83
86
|
|
|
84
87
|
|
|
85
88
|
def test_pack_dirs_resolve_in_the_source_layout():
|
|
@@ -87,6 +90,38 @@ def test_pack_dirs_resolve_in_the_source_layout():
|
|
|
87
90
|
assert (pythia.SKILLS_DIR / "pythia-apply" / "SKILL.md").is_file()
|
|
88
91
|
|
|
89
92
|
|
|
93
|
+
def test_agent_user_json_saves_the_same_password_it_prints():
|
|
94
|
+
"""The agent flow: one --json --save run must be self-consistent — the
|
|
95
|
+
password inside the SQL is the password written to connections.json."""
|
|
96
|
+
import argparse
|
|
97
|
+
import contextlib
|
|
98
|
+
import io
|
|
99
|
+
import os
|
|
100
|
+
with tempfile.TemporaryDirectory() as td:
|
|
101
|
+
root = pathlib.Path(td)
|
|
102
|
+
(root / ".pythia").mkdir()
|
|
103
|
+
(root / ".pythia" / "connections.json").write_text(json.dumps(
|
|
104
|
+
{"dev": {"host": "h", "port": 1521, "service_name": "s",
|
|
105
|
+
"user": "OWNER", "password": "p", "schema": "OWNER"}}),
|
|
106
|
+
encoding="utf-8")
|
|
107
|
+
cwd = os.getcwd()
|
|
108
|
+
os.chdir(td)
|
|
109
|
+
try:
|
|
110
|
+
ns = argparse.Namespace(conn=None, json=True, save=True, name=None)
|
|
111
|
+
buf = io.StringIO()
|
|
112
|
+
with contextlib.redirect_stdout(buf):
|
|
113
|
+
pythia.cmd_agent_user(None, None, ns)
|
|
114
|
+
d = json.loads(buf.getvalue())
|
|
115
|
+
assert d["saved_connection"] == "dev_agent"
|
|
116
|
+
assert d["password"] in d["sql"]
|
|
117
|
+
cfg = json.loads((root / ".pythia" / "connections.json")
|
|
118
|
+
.read_text(encoding="utf-8"))
|
|
119
|
+
assert cfg["dev_agent"]["password"] == d["password"]
|
|
120
|
+
assert cfg["dev"]["password"] == "p" # owner untouched
|
|
121
|
+
finally:
|
|
122
|
+
os.chdir(cwd)
|
|
123
|
+
|
|
124
|
+
|
|
90
125
|
def test_one_version_across_every_manifest():
|
|
91
126
|
"""pyproject, plugin.json, marketplace.json and npm/package.json must all
|
|
92
127
|
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.
|
|
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
|
|
@@ -8,6 +8,9 @@ import pathlib
|
|
|
8
8
|
import sys
|
|
9
9
|
import tempfile
|
|
10
10
|
|
|
11
|
+
import os
|
|
12
|
+
os.environ.setdefault("PYTHIA_CI", "1") # the suites are headless by design
|
|
13
|
+
|
|
11
14
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "scripts"))
|
|
12
15
|
import pythia # noqa: E402
|
|
13
16
|
|
|
@@ -558,6 +561,103 @@ def test_journal_prune_keeps_applied_entries():
|
|
|
558
561
|
assert pythia.list_journal_entries(td) == [kept]
|
|
559
562
|
|
|
560
563
|
|
|
564
|
+
def test_agent_user_alter_form_and_warning_prediction():
|
|
565
|
+
"""DB-aware agent-user: an existing agent user gets the ALTER+UNLOCK
|
|
566
|
+
form (CREATE would be ORA-01920), and a dirty owner is called out —
|
|
567
|
+
check will STILL warn — before the DBA runs anything."""
|
|
568
|
+
import argparse
|
|
569
|
+
import contextlib
|
|
570
|
+
import io
|
|
571
|
+
import os
|
|
572
|
+
script = {"from all_users": ([("USERNAME",)], [("APP_AGENT",)]),
|
|
573
|
+
"from session_privs": ([("PRIVILEGE",)], [("DROP ANY TABLE",)])}
|
|
574
|
+
with tempfile.TemporaryDirectory() as td:
|
|
575
|
+
(pathlib.Path(td) / ".pythia").mkdir()
|
|
576
|
+
(pathlib.Path(td) / ".pythia" / "connections.json").write_text(
|
|
577
|
+
json.dumps({"dev": {"host": "h", "user": "APP", "password": "p",
|
|
578
|
+
"schema": "APP"}}), encoding="utf-8")
|
|
579
|
+
cwd = os.getcwd()
|
|
580
|
+
os.chdir(td)
|
|
581
|
+
try:
|
|
582
|
+
ns = argparse.Namespace(conn=None, json=True, save=False, name=None)
|
|
583
|
+
buf = io.StringIO()
|
|
584
|
+
with contextlib.redirect_stdout(buf):
|
|
585
|
+
pythia.cmd_agent_user(FakeConn(script), "APP", ns)
|
|
586
|
+
d = json.loads(buf.getvalue())
|
|
587
|
+
finally:
|
|
588
|
+
os.chdir(cwd)
|
|
589
|
+
assert d["agent_user_exists"] is True
|
|
590
|
+
assert "ALTER USER APP_AGENT IDENTIFIED BY" in d["sql"]
|
|
591
|
+
assert "ACCOUNT UNLOCK" in d["sql"]
|
|
592
|
+
assert "CREATE USER" not in d["sql"]
|
|
593
|
+
assert d["check_will_warn"] is True
|
|
594
|
+
assert d["owner_dangerous_privs"] == ["DROP ANY TABLE"]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class _NoTTY:
|
|
598
|
+
"""A headless agent session: PYTHIA_CI cleared, stdin not a terminal."""
|
|
599
|
+
def __enter__(self):
|
|
600
|
+
import io
|
|
601
|
+
self.ci = os.environ.pop("PYTHIA_CI", None)
|
|
602
|
+
self.stdin = sys.stdin
|
|
603
|
+
sys.stdin = io.StringIO()
|
|
604
|
+
return self
|
|
605
|
+
|
|
606
|
+
def __exit__(self, *a):
|
|
607
|
+
sys.stdin = self.stdin
|
|
608
|
+
if self.ci is not None:
|
|
609
|
+
os.environ["PYTHIA_CI"] = self.ci
|
|
610
|
+
return False
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def test_headless_yes_is_refused_before_anything_is_written():
|
|
614
|
+
"""--yes belongs to a human at a terminal. An agent (no TTY) must be
|
|
615
|
+
told to preview, stop, and wait for the developer — and nothing may
|
|
616
|
+
reach the database."""
|
|
617
|
+
with tempfile.TemporaryDirectory() as td, _NoTTY():
|
|
618
|
+
conn = FakeConn(base_script())
|
|
619
|
+
try:
|
|
620
|
+
pythia.run_apply(conn, "APP", apply_ns(td, file="f.sql", yes=True),
|
|
621
|
+
NEW_FILE)
|
|
622
|
+
except SystemExit as e:
|
|
623
|
+
msg = str(e)
|
|
624
|
+
assert "developer" in msg and "--confirm" in msg
|
|
625
|
+
assert "PYTHIA_CI" in msg
|
|
626
|
+
else:
|
|
627
|
+
raise AssertionError("expected SystemExit, none raised")
|
|
628
|
+
assert wrote_ddl(conn) == [] # refused before the write
|
|
629
|
+
# the developer at a real terminal keeps --yes (simulated via PYTHIA_CI)
|
|
630
|
+
with tempfile.TemporaryDirectory() as td:
|
|
631
|
+
conn = FakeConn(base_script())
|
|
632
|
+
assert pythia.run_apply(conn, "APP",
|
|
633
|
+
apply_ns(td, file="f.sql", yes=True),
|
|
634
|
+
NEW_FILE) == 0
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def test_headless_policy_loosening_is_refused_tightening_allowed():
|
|
638
|
+
import argparse
|
|
639
|
+
import contextlib
|
|
640
|
+
import io
|
|
641
|
+
|
|
642
|
+
def policy_ns(root, group, value):
|
|
643
|
+
return argparse.Namespace(action="set", group=group, value=value,
|
|
644
|
+
json=False, project_root=root)
|
|
645
|
+
|
|
646
|
+
with tempfile.TemporaryDirectory() as td, _NoTTY():
|
|
647
|
+
# deny -> confirm is loosening: the developer's decision
|
|
648
|
+
try:
|
|
649
|
+
pythia.cmd_policy(None, None, policy_ns(td, "structural", "confirm"))
|
|
650
|
+
except SystemExit as e:
|
|
651
|
+
assert "developer" in str(e) and "policy set structural" in str(e)
|
|
652
|
+
else:
|
|
653
|
+
raise AssertionError("expected SystemExit, none raised")
|
|
654
|
+
assert not pythia.policy_path(td).is_file() # nothing written
|
|
655
|
+
# confirm -> deny is tightening: always allowed, even headless
|
|
656
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
657
|
+
pythia.cmd_policy(None, None, policy_ns(td, "plsql_source", "deny"))
|
|
658
|
+
assert pythia.load_policy(td)["plsql_source"][0] == "deny"
|
|
659
|
+
|
|
660
|
+
|
|
561
661
|
def main():
|
|
562
662
|
failed = 0
|
|
563
663
|
for name, fn in sorted(globals().items()):
|
|
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
|
|
File without changes
|
{pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/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
|
{pythia_plsql-0.2.0 → pythia_plsql-0.3.0}/skills/pythia-explore/reference/data-dictionary.md
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
|