mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
merge(docs): MkDocs site with flow diagrams; RustFS permissions
14 Mermaid diagrams over 8 pages (overview, scanning, plane, memory, RustFS setup with the complete agent policy, memory-and-detection, evaluation, ops); strict build passes; uv.lock carries the docs extra. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
@@ -33,3 +33,6 @@ vector-store/
|
||||
# own scratch dir so it never lands here, but a maintainer querying a CPG by hand from the repo root
|
||||
# will create one.
|
||||
workspace/
|
||||
|
||||
# MkDocs build output (mkdocs build)
|
||||
site/
|
||||
|
||||
@@ -221,6 +221,8 @@ shell, Docker or a free-form command.
|
||||
|
||||
## Further reading
|
||||
|
||||
Documentation site with flow diagrams (`docs/`, `mkdocs.yml`): `uv sync --extra docs && uv run mkdocs serve`.
|
||||
|
||||
- [docs/architecture.md](docs/architecture.md): the scan pipeline, evidence ladder, CWE policy
|
||||
and project score, and the self-improving loops.
|
||||
- [docs/evaluation.md](docs/evaluation.md): independent populations, the qualification gates,
|
||||
|
||||
@@ -80,6 +80,6 @@ v3 check are still ahead.
|
||||
| `ousast learn curve --program ID` | the metric over memory size on a fixed candidate subset |
|
||||
| `ousast learn audit-leaks` | how well each signal alone separates the sides of a pair (no model) |
|
||||
|
||||
The memory store is the plane's (`OUSAST_MEMORY`; see [ops/ax/README.md](../ops/ax/README.md)).
|
||||
The memory store is the plane's (`OUSAST_MEMORY`; see [ops/ax/README.md](ops/ax/README.md)).
|
||||
The harvest that produced the plane signals runs as plane Runs generated by
|
||||
`ousast plane harvest` (`benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json`).
|
||||
|
||||
@@ -49,6 +49,43 @@ v3 is reserved: development and analysis work never reads its cases, and
|
||||
`tests/test_independent_population.py` fails when anything in the development tree (a recipe,
|
||||
catalog, manifest or measurement) names one of its repositories.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Frozen: cases selected and frozen before any scan
|
||||
Frozen --> Scored: scored once under the pre-registered protocol
|
||||
Scored --> Spent: result recorded
|
||||
Spent --> [*]: may inform changes, can no longer qualify anything
|
||||
note right of Frozen
|
||||
v3 (PHP, 15 cases) waits here as the one-time final check,
|
||||
run once after a prediction is committed
|
||||
end note
|
||||
note right of Spent
|
||||
v1 and v2 are spent
|
||||
end note
|
||||
```
|
||||
|
||||
## Folds: how learned decisions are kept out of their own training data
|
||||
|
||||
The decision engine is evaluated on repositories it was not shown (`src/openultrasast/learn/folds.py`).
|
||||
Every split is by repository group (the label builder's normalised `owner/name`, merged across
|
||||
URLs, forks and shared advisories), across all corpora, seeded and deterministic.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
groups["All repository groups with labels"] --> c["Compile split: 25% of groups, never evaluated"]
|
||||
groups --> rest["The other 75%"]
|
||||
c --> boot["C_boot 60%: demonstrations"]
|
||||
c --> val["C_val 40%: instruction scoring"]
|
||||
rest --> outer["Outer grouped 5-fold: calibration cross-fitted, operating points nested"]
|
||||
rest --> loso["Leave-one-source-out: a source held out whole"]
|
||||
rest --> lofo["Leave-one-framework-out: frameworks with at least 10 groups"]
|
||||
```
|
||||
|
||||
`assert_disjoint` runs before a compile: the compile split and the evaluation folds share no
|
||||
group. A framework with fewer than 10 groups gets `insufficient data` instead of a fold. As of
|
||||
2026-10-02 only the outer folds of the injection family have been run
|
||||
([decision-engine.md](decision-engine.md#first-measured-slice-2026-10-01)).
|
||||
|
||||
## The plane increment
|
||||
|
||||
The first increment of the agentic plane ran the model-driven pipeline's validation set (46
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ uv run ousast improve benchmarks/manifests/java-spring-boot-vulnerable.toml
|
||||
## 7. Running the agentic plane
|
||||
|
||||
Model work over whole repositories runs as a Run manifest on ax, one isolated actor
|
||||
per task (bring-up: [ops/ax/README.md](../ops/ax/README.md)). The provider key is
|
||||
per task (bring-up: [ops/ax/README.md](ops/ax/README.md)). The provider key is
|
||||
read from the environment (or `.env`, which never overrides an exported variable) by
|
||||
`ousast` and sent only in each task's start request.
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# OpenUltraSAST
|
||||
|
||||
OpenUltraSAST is an experimental static security analyser meant to work as a safety net on
|
||||
repositories it has never seen: it scans a checkout, or the commits a `git push` would publish,
|
||||
and separates what it can prove from what it merely suspects. A scan that could not look at
|
||||
something records a degradation instead of reporting a clean result.
|
||||
|
||||
**State as of 2026-10-02 (v1.2.0-alpha.1):** the pre-push safety net is **NO-GO** for rollout.
|
||||
No hook capability is enabled, and no independent population has passed the qualification gates
|
||||
(see [Evaluation](evaluation.md)).
|
||||
|
||||
## Three layers
|
||||
|
||||
| Layer | What it is | Where |
|
||||
| --- | --- | --- |
|
||||
| **Scanners** | `ousast scan` (`quick`, `standard`, `deep`) and `ousast pre-push`: language-scoped pattern rules, a Joern code property graph engine, verification and scoring. Deterministic in `quick`; no model is needed for a scan. | [Scanning](scanning.md), [Architecture](architecture.md) |
|
||||
| **The agentic plane on ax** | Model-driven work over whole repositories, run as a DAG of tasks on google/ax, one isolated actor per task. What the runs learn is kept in a memory store. Maintainer surface: `ousast plane`. | [The plane on ax](plane.md), [Memory](memory.md) |
|
||||
| **The decision engine** | A learned classifier that weighs every instrument's signals against similar labelled cases from memory. **In development, not adopted**: no scan, pre-push check or report uses it. Maintainer surface: `ousast learn`. | [Memory and detection](memory-and-detection.md), [Decision engine](decision-engine.md) |
|
||||
|
||||
## How they connect
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
repo["Repository or push delta"] --> scan["Scanners: quick rules, Joern engine, verify, score"]
|
||||
scan --> report["Report: findings with evidence, SARIF, score"]
|
||||
scan -- "alerts on vulnerable and fixed pins" --> plane["Plane on ax: repo-facts, verify a/b/c, agree, features"]
|
||||
plane -- "remember" --> mem[("Memory store: rows, facts, examples, blobs")]
|
||||
mem -- "improve --memory: rule-status proposals" --> gate{"Validator and detection gate"}
|
||||
gate -- "accepted round" --> ledger["Ruleset ledger: rule status"]
|
||||
ledger --> scan
|
||||
labels["Labels from ground truth"] --> mem
|
||||
mem -- "retrieval of similar labelled cases" --> engine["Decision engine: compiled AI classifier"]
|
||||
engine -. "intended, not adopted: BLOCK / ADVISORY" .-> report
|
||||
```
|
||||
|
||||
- The **scanners** produce findings and, inside plane Runs, the `alerts` that become memory rows.
|
||||
- The **plane** runs model work (verify passes, roles) and model-free bookkeeping (facts, agreement,
|
||||
feature records) per case; its `remember` task turns a case's artifacts into rows of the
|
||||
[memory store](memory.md).
|
||||
- Memory feeds two consumers. `ousast improve --memory` turns stored rows into rule-status
|
||||
proposals that pass through the same validator and gate as every other edit. The decision engine
|
||||
retrieves labelled examples from the same store.
|
||||
- The decision engine's output (dotted line) is the intended path. Nothing user-facing reads it
|
||||
today.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- Run a scan: [Examples](examples.md), and the commands in [Scanning](scanning.md).
|
||||
- Understand a finding's evidence and score: [Architecture](architecture.md).
|
||||
- Operate the plane and its store: [ax on this host](ops/ax/README.md) and
|
||||
[RustFS setup](rustfs.md).
|
||||
- Trust boundaries and hardening: [Threat model](threat-model.md).
|
||||
|
||||
Every number in these pages is quoted from a committed record whose path is given next to it.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Memory and detection
|
||||
|
||||
How what the system remembers is meant to turn into better decisions on code it has not seen.
|
||||
This is the design of the learned decision engine (`src/openultrasast/learn/`), which is **in
|
||||
development and not adopted**: no scan, pre-push check or report uses it today. Every user-facing
|
||||
decision is still the deterministic one described in [Scanning](scanning.md) and
|
||||
[Architecture](architecture.md). The engine's commands and status are on
|
||||
[Decision engine](decision-engine.md); the store itself is on [Memory](memory.md).
|
||||
|
||||
## From instruments to a decision
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
inst["Instruments: quick rules, Joern engine, verify passes and agree, model roles, repo facts"] --> feat["Feature record per candidate: path, function, family"]
|
||||
feat --> prof["Input profile v1: engine and function length withheld"]
|
||||
lab["Labels from ground truth: pairs, populations v1/v2, advisory fixes, adjudications"] --> ex["Example rows in memory: label + features + excerpt"]
|
||||
prof --> ret["Retrieval: signal profile, then code embeddings"]
|
||||
ex --> ret
|
||||
ret --> bnd{"Evaluation boundary"}
|
||||
bnd --> prog["Compiled AI classifier: instruction, demonstrations, up to 6 examples, candidate"]
|
||||
prog --> score["Score s: mean over k samples"]
|
||||
score --> cal["Platt calibration, cross-fitted on held-out repositories"]
|
||||
cal --> op{"Operating point"}
|
||||
op -- "calibration holds and precision reachable" --> block["BLOCK"]
|
||||
op -- "otherwise" --> adv["ADVISORY"]
|
||||
```
|
||||
|
||||
The intended place for these decisions is changed code: `learn/features.py` builds feature records
|
||||
for a whole-repository scan or for a base..head delta (`build_for_scan` with `Delta`), the same
|
||||
delta `ousast pre-push` checks. Integration into scan and pre-push is still ahead.
|
||||
|
||||
## 1. Instruments produce signals, not verdicts
|
||||
|
||||
Each instrument contributes one block to a candidate's feature record (`learn/schema.py`,
|
||||
`INSTRUMENTS`): `language`, `quick` (quick rules), `engine` (the Joern model layer), `facts` (repo
|
||||
facts, such as callers and function span), `source`, `entry_points`, `roles` (model source, sink
|
||||
and sanitizer roles), `delta`, `verify` and `agree` (the plane's verify passes and their
|
||||
agreement), and `model_sinks`. A candidate is `(path, function, family)` of one repository and pin
|
||||
where at least one instrument emitted a signal of that family (`learn/features.py`).
|
||||
|
||||
An instrument that did not run, had no coverage or failed records that state (`none`, `failed`,
|
||||
`not_applicable`), never a zero, so "silent" and "did not look" stay distinguishable. None of the
|
||||
instruments decides alone.
|
||||
|
||||
**Why profile v1 withholds the engine and function length.** A leak audit
|
||||
(`ousast learn audit-leaks`, `benchmarks/measurements/2026-10-01-decision-engine-leak-audit/`)
|
||||
found that both separate the vulnerable and fixed sides of a pair by construction: a fix removes
|
||||
a sink, and a guard lengthens the function. That holds on the pair corpus, not on unseen code, so
|
||||
`INPUT_PROFILES["v1"]` drops the `engine` block and `facts.function_lines` from prompts and from
|
||||
the retrieval distance.
|
||||
|
||||
## 2. Labels come from ground truth only
|
||||
|
||||
`ousast learn labels` (`learn/labels.py`) reads only the sources on a fail-closed allow-list
|
||||
(`learn/sources.toml`): `population-v1`, `population-v2`, `dev-php` (reviewed recipe sites),
|
||||
`pairs`, `advisory-fixes`, `adjudications` and `assumed_benign`. A file the list does not name is
|
||||
refused before it is opened; the reserved population v3 is excluded unread.
|
||||
|
||||
- **Positives** are declared sites and the vulnerable side of a fix; **negatives** are the same
|
||||
function on the fixed side where the fix changed it, and recorded `False`/`FP` adjudications.
|
||||
A vulnerable-pin candidate matching no site is unlabelled, never negative.
|
||||
- **Assumed benign** (maintainer decision 2026-09-30): ordinary non-security commits of the training
|
||||
repositories, at least 90 days from any fix, with no later security commit touching the same
|
||||
files. They are their own source with weight 0.5, conditional on a signal at the tip and none at
|
||||
the base, and reported separately, never merged with verified negatives.
|
||||
- **Plane verdicts are features, not labels.** "Agreed", "rejected" or "disputed" is the output of
|
||||
the verify instrument; it enters the record as the `verify` and `agree` blocks. Using it as a
|
||||
label would teach the engine to reproduce the plane, mistakes included, so "rejected by the
|
||||
plane" is never a negative (`learn/labels.py`: "A plane verdict ... is never a label").
|
||||
|
||||
`ousast learn memory build` joins each label with the candidate's feature record and an excerpt of
|
||||
its function and stores one `example` row (no rationale) plus the excerpt blob.
|
||||
|
||||
## 3. Retrieval of similar labelled cases
|
||||
|
||||
`learn/retrieve.py`, no model:
|
||||
|
||||
1. **Signal profile.** A Gower distance over the allow-listed features of the active profile,
|
||||
per instrument block, with missing values explicit (a different state counts as distance 1).
|
||||
The nearest 40 examples of the candidate's family and the nearest 40 of other families go on.
|
||||
2. **Code embeddings.** A cosine re-rank of the excerpts' embeddings (OpenAI
|
||||
`text-embedding-3-small` through OpenRouter, 1,536 dimensions, cached by excerpt hash):
|
||||
`r = 0.5 (1 - D) + 0.5 cos`. Without the candidate's vector the retrieval is signals only.
|
||||
3. **Balance.** Six examples: at most half per label, at most 2 per repository group, at most 2
|
||||
contrast examples from other families.
|
||||
|
||||
**The evaluation boundary.** `eligible()` is the only way an example enters a prompt, for retrieved
|
||||
examples and compiled demonstrations alike: never the candidate's own repository group, never a
|
||||
group the fold evaluates, never the held-out source or framework, and (outside deployment) never
|
||||
a near-duplicate of the candidate (cosine >= 0.98). The program repeats the check on the ids it
|
||||
actually rendered before any call.
|
||||
|
||||
## 4. The AI classifier, compiled
|
||||
|
||||
`learn/program.py` and `learn/compile.py`, DSPy-style and in-house. The prompt is the instruction
|
||||
plus the compiled demonstrations (a prefix identical for every candidate, so the provider's prefix
|
||||
cache hits), then the retrieved examples, then the candidate's excerpt, signals, roles and family.
|
||||
The model (`deepseek-flash`) answers `vulnerable`, `not_vulnerable` or `unsure` with a confidence
|
||||
and a rationale that must cite a line of the code. It is sampled `k` times; the score `s` is the
|
||||
mean over samples, and a majority `unsure` can never BLOCK. Compilation bootstraps demonstrations
|
||||
and searches instructions on a compile split of repository groups, scored by a pre-registered
|
||||
balanced Brier score (`learn/compile.toml`); the program is stored as `programs/<id>.json`.
|
||||
|
||||
## 5. Calibration and the two operating points
|
||||
|
||||
`learn/calibrate.py`: Platt scaling of `s`, cross-fitted so that no map is checked on the fold it
|
||||
was fitted on. Calibration **holds** when the slope interval contains 1, the intercept interval
|
||||
contains 0 and ECE <= 0.05. **BLOCK** is offered only when calibration holds and a fold reaches the
|
||||
required precision; otherwise the engine offers **ADVISORY** only.
|
||||
|
||||
## The loop: memory improves decisions without retraining
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
new["New labels and adjudications"] --> labels["ousast learn labels"]
|
||||
labels --> build["ousast learn memory build, embed"]
|
||||
build --> mem[("Memory store: example rows")]
|
||||
mem --> retr["Retrieved per candidate at decision time"]
|
||||
retr --> prog["Same compiled program"]
|
||||
prog --> dec["Decision"]
|
||||
runs["Plane runs: alert, verdict, coverage rows"] --> mem2[("Memory store: run rows")]
|
||||
mem2 --> imp["ousast improve --memory: M1, M2 proposals"]
|
||||
imp --> gate{"Unchanged validator and gate"}
|
||||
gate -- "accepted round" --> ledger["Rule status in the ledger"]
|
||||
```
|
||||
|
||||
- **Decisions.** The compiled part of the program is its instruction and demonstrations; the
|
||||
examples are retrieved from memory for each candidate. A new `example` row is therefore eligible
|
||||
for retrieval in the next decision without recompiling the program, subject to the same
|
||||
evaluation boundary. `ousast learn curve` measures the metric over memory size on a fixed
|
||||
candidate subset.
|
||||
- **Rules.** `ousast improve --memory` reads the run rows (alerts, verdicts, coverage) and proposes
|
||||
rule-status edits only (shadow a rule that is repeatedly false across repositories; re-enable a
|
||||
shadow rule that keeps hitting agreed declared sites). Rows from holdout pairs, the gated
|
||||
manifest's own cases and any `--qualify-population` are dropped first, and every proposal goes
|
||||
through the same validator and gate as any other edit (`improve/memory.py`).
|
||||
|
||||
## What the first slice measured
|
||||
|
||||
Record: `benchmarks/measurements/2026-10-01-decision-engine-injection-slice/record.json` (counts
|
||||
only). Family **injection**; memory of 724 static examples from all families; input profile v1;
|
||||
framework priors off; out-of-repository evaluation (compile split never evaluated, outer grouped
|
||||
5-fold, calibration cross-fitted, operating points nested); 95% repository-cluster bootstrap
|
||||
intervals.
|
||||
|
||||
| Measure | Value | What it means |
|
||||
| --- | --- | --- |
|
||||
| Candidates | 110 in 29 repository groups, 58 positive | the positive share is 58/110 = 0.53 |
|
||||
| AUC of the score `s` | 0.74 (0.65 - 0.85) | the score ranks a vulnerable candidate above a non-vulnerable one about three times in four: there is ranking signal |
|
||||
| ADVISORY | recall 0.90 (0.82 - 0.97), precision 0.54 (0.44 - 0.65) | precision is at the positive share (0.53), so at this threshold ADVISORY is **not discriminating**: it flags nearly as indiscriminately as flagging everything would |
|
||||
| BLOCK | not offered | calibration does not hold (ECE 0.064 > 0.05), and every outer fold reported `precision_unreachable` |
|
||||
|
||||
**The engine is not adopted.** These numbers come from one family and one slice; the other
|
||||
families, the leave-one-source-out and leave-one-framework-out evaluations, integration into scan
|
||||
and pre-push, and the one-time check on population v3 are still ahead
|
||||
([Decision engine](decision-engine.md#first-measured-slice-2026-10-01)).
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
# Memory
|
||||
|
||||
One store, keyed by repository and pin, holds what plane runs and the decision engine learned.
|
||||
The code is `src/openultrasast/plane/memory.py`; the same layout serves both backends.
|
||||
How the stored rows feed detection is on [Memory and detection](memory-and-detection.md).
|
||||
|
||||
## What is stored
|
||||
|
||||
```
|
||||
index.jsonl one row per ingested (run, task): sha256 of its rows, row count
|
||||
facts/<sha256>.json repository facts by content (identical facts stored once)
|
||||
repos/<host>__<owner>__<name>/<pin>.jsonl the rows of one repository + 40-hex pin, sorted by id
|
||||
excerpts/<sha256>.txt code excerpts, by content
|
||||
embeddings/<model>/<excerpt sha>.json an excerpt's embedding vector, per embedding model
|
||||
responses/<request sha>.json cached model responses
|
||||
programs/<program id>.json compiled decision-engine programs
|
||||
```
|
||||
|
||||
**Rows.** Every row carries `id`, `kind`, `repo`, `pin`, `run`, `task`, `population`, `split`
|
||||
and `image` (`ROW_FIELDS`); `id` is the sha256 of (kind, run, task, subject), so a repeated row
|
||||
replaces itself. The kinds (`KINDS`) and who writes them:
|
||||
|
||||
| Kind | Written by | Holds |
|
||||
| --- | --- | --- |
|
||||
| `facts` | `remember` task | the facts entry of a case (points at `facts/<sha256>.json`; reused by memory key) |
|
||||
| `verdict` | `remember` task | per candidate: each verify pass's answer, the final outcome, whether a tie-break ran, cost |
|
||||
| `unit_cost` | `remember` task | per verify pass and file: usd, calls and token usage |
|
||||
| `alert` | `remember` task | a quick-scan or engine alert on the vulnerable or fixed pin (loop Runs) |
|
||||
| `coverage` | `remember` task | per language and pin: whether any rule could have fired there |
|
||||
| `features` | `remember` task, from the `features` task | one feature record per candidate and family |
|
||||
| `proposal_outcome` | `ousast improve --memory` (not with `--dry-run`), and the loop's `improve` step | what happened to each memory proposal in a round |
|
||||
| `example` | `ousast learn memory build` | a labelled case: label + feature record + excerpt sha (no rationale) |
|
||||
| `label`, `decision`, `experiment`, `arm_outcome`, `experiment_result` | reserved for later decision-engine tasks | declared in `KINDS`; not written by the code on main yet |
|
||||
|
||||
**Blobs** (`put_blob`) are content-addressed: a 64-hex name under a fixed prefix. Excerpts are
|
||||
written by `learn memory build` (`learn/examples.py`), embeddings by `learn memory embed`
|
||||
(`learn/embeddings.py`, keyed by excerpt sha so an excerpt is embedded once), responses by the
|
||||
program's caller (`learn/program.py`, keyed by the sha256 of model, parameters digest, messages,
|
||||
temperature, sample and format, so a rerun replays at no cost), and compiled programs by
|
||||
`learn compile` (`learn/compile.py`).
|
||||
|
||||
## Write path
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
tasks["Plane tasks: facts, verify a/b/c, agree, alerts, features"] --> rem["remember task: memory.jsonl"]
|
||||
rem --> recv["Delivered to the host with the run's artifacts"]
|
||||
recv --> ingest["ingest after ousast plane run, or ousast plane remember RUN"]
|
||||
loop["Loop improve step: proposal_outcome"] --> ingest
|
||||
improve["ousast improve --memory: proposal_outcome"] --> put
|
||||
build["ousast learn memory build: labels + features + excerpts"] --> put["put_rows, put_blob"]
|
||||
embed["ousast learn memory embed"] --> put
|
||||
compile["ousast learn compile, evaluate"] --> put
|
||||
ingest --> idx{"index.jsonl: same run, task and sha256?"}
|
||||
idx -- "yes" --> skip["Skipped"]
|
||||
idx -- "no" --> put
|
||||
put --> store[("Memory store")]
|
||||
```
|
||||
|
||||
- `ingest` validates every row first: a malformed file fails before anything is written.
|
||||
- Rows are merged into their repository + pin object by `id`. Every object is written whole and the
|
||||
index last, so an interrupted ingest leaves nothing a repeat would not repair.
|
||||
- Each row object carries tags (`TAG_FIELDS`: `repo`, `pin`, `kind`, `family`, `run`,
|
||||
`population`, `split`) listing the distinct values of its rows (`*` when they exceed 256
|
||||
characters).
|
||||
- The plane's store failures never change a run's result: an ingest can be repeated by hand
|
||||
(`cli.py`, `_plane_memory`).
|
||||
|
||||
## Read path
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
store[("Memory store")] --> seed["seed before a Run"]
|
||||
seed --> reuse["Facts reused by memory key: repo, pin, candidates, runner image"]
|
||||
seed --> snap["memory-snapshot: rows after the train-on-test guard"]
|
||||
store --> examples["learn: load example rows, excerpts, embeddings"]
|
||||
examples --> retr["Retrieval of similar labelled cases"]
|
||||
store --> responses["Response cache: replay at no cost"]
|
||||
store --> imp["ousast improve --memory: alert, verdict, coverage rows"]
|
||||
imp --> guard["Train-on-test guard"]
|
||||
guard --> rules["Rules M1, M2 to rule-status proposals"]
|
||||
rules --> gate{"Unchanged validator and gate"}
|
||||
```
|
||||
|
||||
- **Seed** (`memory.seed`): a Run task whose `openultrasast.io/memory-key` matches stored facts gets
|
||||
them and is marked done; a changed pin, candidate set or runner image finds nothing and
|
||||
recomputes. The loop's `memory-snapshot` is seeded the same way, because a sandboxed task cannot
|
||||
read the store.
|
||||
- **Retrieval** (`learn/retrieve.py`): see
|
||||
[Memory and detection](memory-and-detection.md#3-retrieval-of-similar-labelled-cases).
|
||||
- **`improve --memory`** (`improve/memory.py`): rows from holdout pairs, from the gated manifest's
|
||||
own cases and from any `--qualify-population` are dropped before any rule sees them. See
|
||||
[Architecture](architecture.md#proposals-from-plane-memory).
|
||||
|
||||
## Backends: FileStore and the S3 store
|
||||
|
||||
`OUSAST_MEMORY` selects the backend (`open_store`):
|
||||
|
||||
| | `FileStore` | `MinioStore` (S3: RustFS or MinIO) |
|
||||
| --- | --- | --- |
|
||||
| Selected by | `file:///path`, or nothing (default `$OUSAST_RESULTS/plane/memory`) | `minio://<bucket>[/<prefix>]` |
|
||||
| Needs | a local disk; refuses to write below 1 GiB free | the `minio` extra, `MINIO_*` settings from `.env` or the environment |
|
||||
| Filtered reads | read and filter locally | S3 Select pushdown (`SelectObjectContent` over JSON Lines); kind filter by object tag |
|
||||
| Object version cited by provenance | the file's content hash | the bucket's object version id |
|
||||
| Bucket setup | none | done once by an admin, verified at every start ([RustFS setup](rustfs.md)) |
|
||||
|
||||
### Verify at startup, no fallback
|
||||
|
||||
The S3 store never configures its bucket. Each time a `MinioStore` is opened for a bucket and
|
||||
prefix (once per process), `verify_bucket()` checks, using reads plus one small probe object at
|
||||
`<prefix>/_probe/select.jsonl`:
|
||||
|
||||
1. versioning is `Enabled` (provenance cites object versions);
|
||||
2. an enabled lifecycle rule expires `<prefix>/runs/` after a number of days, as a plain prefix
|
||||
filter, and no enabled rule expires the whole store;
|
||||
3. S3 Select answers a probe query over JSON Lines;
|
||||
4. the probe's object tags are readable.
|
||||
|
||||
Anything missing is named, with the admin command that fixes it, in one `MemoryStoreError`, and
|
||||
the store does not start. There is no local or fetch-and-filter fallback: a filtered read that
|
||||
S3 Select cannot answer raises a `MemoryStoreError` instead of returning "no rows". The exact
|
||||
messages are listed on [RustFS setup](rustfs.md#what-the-store-verifies-at-startup).
|
||||
|
||||
### Queryable fields per kind (in progress)
|
||||
|
||||
RustFS's Select infers an object's JSON schema from its leading rows, so a `where` field missing
|
||||
there fails with `EvaluatorBindingDoesNotExist` even when a later row has it (measured
|
||||
2026-10-02; `memory.py`, `MinioStore._select`). On main today the store turns that failure into a
|
||||
`MemoryStoreError` naming the field, never into an empty answer. A fixed set of queryable fields
|
||||
per row kind, so that every filtered field is present in every row of its kind, is in progress
|
||||
and not on main as of this page.
|
||||
@@ -0,0 +1,3 @@
|
||||
--8<-- "ops/README.md"
|
||||
|
||||
*Source: `ops/README.md` in the repository; this page includes it when the docs site is built.*
|
||||
@@ -0,0 +1,10 @@
|
||||
# ax on this host
|
||||
|
||||
The agentic plane runs every task on google/ax over Agent Substrate in a single-node kind cluster. ax is the
|
||||
only executor; `ousast plane run` submits, resumes, starts and collects. The flow is drawn in
|
||||
[The plane on ax](../../plane.md); the bucket setup for the S3 memory store is in
|
||||
[RustFS setup](../../rustfs.md).
|
||||
|
||||
--8<-- "ops/ax/README.md:5"
|
||||
|
||||
*Source: `ops/ax/README.md` in the repository, from its line 5 on; this page includes it when the docs site is built.*
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# The plane on ax
|
||||
|
||||
Model-driven work over whole repositories runs on a separate agentic plane: a **Run**
|
||||
(`plane/runs/*.yaml`, kind `openultrasast.io/v1alpha1 Run`) is a DAG of steps, and each step is
|
||||
an ax **Task** (`plane/tasks/`) that binds its Workspaces (`plane/workspaces/`), at most one ax
|
||||
**Model** (`plane/models/`: `deepseek-flash` for chat, `openrouter-embedding` for embeddings) and
|
||||
its own `budget: {usd, calls}`. ax (google/ax over Agent Substrate, in a single-node kind cluster
|
||||
on the maintainer's host) is the only executor. Bring-up and the host's lessons are in
|
||||
[ax on this host](ops/ax/README.md); this page draws the flow.
|
||||
|
||||
Sources: `src/openultrasast/plane/reconciler.py`, `router.py`, `egress.py`, `runner.py`,
|
||||
`generate.py`, `memory.py`, and `src/openultrasast/plane/tasks/`.
|
||||
|
||||
## One task, end to end
|
||||
|
||||
`ousast plane run RUN.yaml` (the reconciler) does this for every ready task; `--workers` bounds
|
||||
the tasks in flight, and a rerun skips tasks already done.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant R as Reconciler (ousast plane run)
|
||||
participant AX as ax control plane
|
||||
participant EG as Egress gateway (atenet-egress)
|
||||
participant RT as Substrate router (atenet-router)
|
||||
participant RN as Runner (actor in gVisor)
|
||||
participant RC as Artifact receiver (host)
|
||||
R->>R: render Model, Workspaces, Task to task.yaml, read the Model's key from env or .env
|
||||
R->>AX: ax apply -f task.yaml (creates the actor, egress denied)
|
||||
R->>EG: EgressPolicy for this actor only
|
||||
R->>AX: ax resume task (retried while the golden snapshot builds)
|
||||
loop until phase Running
|
||||
R->>AX: ax get task
|
||||
end
|
||||
R->>RT: POST /ousast/v1/start {run, task, credentials}
|
||||
RT->>RN: forwarded to this task's actor only
|
||||
RN->>RN: materialise Workspaces (git at pinned commits), after the start
|
||||
RN->>RC: GET /inputs/producer/artifact (through the gateway)
|
||||
RN->>RN: python -m openultrasast.plane.tasks.MODULE (key in its env only, metered budget)
|
||||
RN->>RC: POST tar of OUSAST_OUTPUT_DIR (through the gateway) = completion
|
||||
RN->>RN: write the completion marker
|
||||
R->>AX: ax delete task, then delete the EgressPolicy
|
||||
R->>R: summary.json into state.json, plane status writes attribution.json
|
||||
```
|
||||
|
||||
What each step guarantees:
|
||||
|
||||
- **Credentials** (`router.py`, `runner.py`). The reconciler reads the variable the Model's
|
||||
`secretKey.key` names from the operator's environment or `.env` (which never overrides an
|
||||
exported variable). The key exists only in the start request; the runner keeps it in memory,
|
||||
passes it to the command's environment, redacts it from echoed stderr and never writes it.
|
||||
- **Egress** (`egress.py`). One EgressPolicy per actor, deny by default, hostnames only: plain
|
||||
HTTP to the artifact receiver, TLS passthrough to the source hosts of the bound Workspaces and
|
||||
to the hosts the bound Model declares (`openultrasast.io/egress-hosts`). It is written between
|
||||
`ax apply` and the resume, and deleted with the actor.
|
||||
- **Inputs** (`reconciler.py`). Artifacts other tasks produced are never rendered into the
|
||||
manifest; the runner fetches them from the receiver after the start, and the receiver serves a
|
||||
running task its declared inputs only.
|
||||
- **Completion** (`runner.py`). ax has no Completed phase: the runner's delivery of the output tar
|
||||
is the task's completion. A completion marker stops a resumed actor from repeating billed work;
|
||||
when the marker says the delivery failed, a later boot retries the delivery alone.
|
||||
- **Budgets** (`plane/budget.py`). The metered client refuses the next call once a task's `usd` or
|
||||
`calls` ceiling is reached; the task ends `unfinished` and resumes on a rerun with a larger
|
||||
budget. Model-free tasks run with `{usd: 0, calls: 0}`, so a stray call fails loudly.
|
||||
- **Attribution.** `ousast plane status RUN` prints per task its status, `calls`, `prompt`,
|
||||
`cache_hit` and `output` tokens and `usd` from the tasks' `summary.json`, and writes
|
||||
`attribution.json` in the run directory (`$OUSAST_RESULTS/plane/RUN/`, default
|
||||
`~/ousast-results/plane/`).
|
||||
|
||||
!!! note "The golden snapshot"
|
||||
Agent Substrate boots every new actor template once as a *golden* actor to snapshot it, with
|
||||
the same image, Task and environment, and inside the sandbox nothing tells that boot from the
|
||||
real one. So the runner's command never starts by itself: it waits for the start request,
|
||||
which the reconciler sends through `atenet-router` to the task's actor, never to the golden
|
||||
one. Workspaces are prepared only after the start, so the golden boot touches no network and a
|
||||
started actor already runs under its egress policy (a clone at boot failed on the live cluster
|
||||
while the actor was being restored, 2026-09-29; `runner.py`, `_run_once`). The first resume of
|
||||
a new image can time out while the snapshot is built, which is why the reconciler retries it
|
||||
for up to `OUSAST_RESUME_TIMEOUT` seconds (900).
|
||||
|
||||
## The Runs: validation and loop
|
||||
|
||||
`ousast plane workspaces POPULATION --validation-set ...` (`generate.py`) writes one chain per
|
||||
case; `--loop` adds the `alerts` step per case and the loop's singleton steps. Every step except
|
||||
the verify passes (and `roles` in harvest Runs) is model-free.
|
||||
|
||||
### Per case
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
facts["facts: repo-facts"] --> va["va: verify pass a"]
|
||||
facts --> vb["vb: verify pass b"]
|
||||
va --> agree["agree: a vs b"]
|
||||
vb --> agree
|
||||
agree -- "disputed only" --> vc["vc: verify pass c"]
|
||||
va --> final["final: 2-of-3"]
|
||||
vb --> final
|
||||
vc --> final
|
||||
final --> features["features: one record per candidate"]
|
||||
alerts["alerts: quick scan on vulnerable and fixed pins (--loop)"] -.-> features
|
||||
features --> remember["remember: memory.jsonl"]
|
||||
alerts -.-> remember
|
||||
final --> remember
|
||||
```
|
||||
|
||||
### The loop (with `--loop`)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
store[("Memory store")] -- "seeded before the Run, after the train-on-test guard" --> snap["memory-snapshot"]
|
||||
rem["remember of every case"] --> measure["loop-measure"]
|
||||
snap --> measure
|
||||
measure --> propose["loop-propose: rules M1 and M2, M3 advisory"]
|
||||
snap -- "index.json" --> propose
|
||||
propose --> improve["loop-improve: one evolve round, unchanged validator and gate"]
|
||||
improve --> outcome["gate.json, journal.json, and rule_policy.json only if accepted"]
|
||||
```
|
||||
|
||||
- **Before the Run** `ousast plane run` seeds from the store (`memory.seed`): a task whose
|
||||
`openultrasast.io/memory-key` (repository, pin, candidates digest, runner image digest) has
|
||||
stored facts gets them and is marked done, and the sandboxed `memory-snapshot` task gets the
|
||||
store's rows after the guard. **After the Run** it ingests every delivered `memory.jsonl`
|
||||
(`ousast plane remember RUN` repeats this by hand). See [Memory](memory.md).
|
||||
- The loop's rules and its guard are described in
|
||||
[Architecture](architecture.md#proposals-from-plane-memory). Adopting an accepted ledger stays a
|
||||
maintainer commit (`src/openultrasast/plane/tasks/loop.py`).
|
||||
- `ousast plane harvest` writes the decision engine's harvest Runs (verify a/b with agree, and
|
||||
model `roles`); `ousast plane alerts-engine` produces a Run's `alerts` from the Joern engine
|
||||
image on the host for PHP and other languages quick mode does not cover.
|
||||
|
||||
## Tasks
|
||||
|
||||
| Task | Model | Does |
|
||||
| --- | --- | --- |
|
||||
| `repo-facts` | none | functions per product file, and each candidate's call sites in other files |
|
||||
| `verify` (passes a, b, c) | `deepseek-flash` | batched tool hunt per file over the candidates, with their known callers |
|
||||
| `agree` | none | a and b agreed or disputed; after pass c on the disputed, 2-of-3 (the step `final`) |
|
||||
| `features` | none | one feature record per candidate for the decision engine |
|
||||
| `remember` | none | the case's artifacts as memory rows |
|
||||
| `alerts` | none | the quick scan on the vulnerable and fixed pins (loop Runs) |
|
||||
| `loop` | none | the steps `snapshot`, `measure`, `propose`, `improve` |
|
||||
| `roles` | `deepseek-flash` | per-repository source, sink and sanitizer roles without a vocabulary |
|
||||
|
||||
Source: [ax on this host](ops/ax/README.md#tasks).
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# RustFS setup for the memory store
|
||||
|
||||
The plane's memory store can live in an S3-compatible bucket: `OUSAST_MEMORY=minio://sast-memory`
|
||||
selects `MinioStore` (`src/openultrasast/plane/memory.py`), which works with RustFS and MinIO. The
|
||||
maintainer's server is RustFS at `files.because-security.com`, bucket `sast-memory`, store at the
|
||||
bucket root. What the store keeps there is described on [Memory](memory.md).
|
||||
|
||||
**The store never configures its bucket.** An admin sets the bucket up once with admin
|
||||
credentials. The agent account the store runs as can read the bucket's settings but not change
|
||||
them, and reads and writes objects freely. Every time the store opens, it checks the setup and
|
||||
refuses to start if anything is missing.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
admin["Admin, once: versioning on, runs/ expiry rule"] --> bucket[("Bucket sast-memory")]
|
||||
agent["Agent account: read settings, read and write objects and tags"] --> verify{"verify_bucket at startup"}
|
||||
bucket --> verify
|
||||
verify -- "all present" --> store["MinioStore serves reads and writes"]
|
||||
verify -- "anything missing" --> refuse["MemoryStoreError naming each missing piece and its admin command"]
|
||||
```
|
||||
|
||||
## One-time admin steps
|
||||
|
||||
Run these with **admin** credentials (not the agent's). Both use the AWS CLI against the RustFS
|
||||
endpoint.
|
||||
|
||||
**1. Turn versioning on.** Provenance cites object version ids.
|
||||
|
||||
```bash
|
||||
aws s3api put-bucket-versioning \
|
||||
--endpoint-url https://files.because-security.com \
|
||||
--bucket sast-memory \
|
||||
--versioning-configuration Status=Enabled
|
||||
```
|
||||
|
||||
**2. Expire raw run outputs under `runs/` after 30 days.** Everything else (`repos/`, `facts/`,
|
||||
`index.jsonl` and the blobs) is kept, so the rule must be a plain prefix filter on `runs/`.
|
||||
|
||||
```bash
|
||||
aws s3api put-bucket-lifecycle-configuration \
|
||||
--endpoint-url https://files.because-security.com \
|
||||
--bucket sast-memory \
|
||||
--lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'
|
||||
```
|
||||
|
||||
`put-bucket-lifecycle-configuration` **replaces every rule on the bucket**. If the bucket already
|
||||
has rules, merge this one into them first. The rule id `ousast-runs-expiry` and the 30 days are the
|
||||
store's defaults (`RUNS_RULE_ID`, `RUNS_EXPIRE_DAYS`); the store accepts any enabled, untagged
|
||||
prefix rule on `runs/` with a number of days.
|
||||
|
||||
To read the result back:
|
||||
|
||||
```bash
|
||||
aws s3api get-bucket-versioning --endpoint-url https://files.because-security.com --bucket sast-memory
|
||||
aws s3api get-bucket-lifecycle-configuration --endpoint-url https://files.because-security.com --bucket sast-memory
|
||||
```
|
||||
|
||||
## The agent policy
|
||||
|
||||
Attach this policy to the agent account (the account whose keys go into `.env`). It is the
|
||||
complete policy: nothing else is needed, and it grants no `Put*` on versioning or lifecycle.
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "BucketSettingsReadOnly",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListBucket",
|
||||
"s3:ListBucketVersions",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetBucketVersioning",
|
||||
"s3:GetLifecycleConfiguration"
|
||||
],
|
||||
"Resource": ["arn:aws:s3:::sast-memory"]
|
||||
},
|
||||
{
|
||||
"Sid": "ObjectsReadWrite",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectVersion",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteObjectVersion",
|
||||
"s3:GetObjectTagging",
|
||||
"s3:PutObjectTagging",
|
||||
"s3:GetObjectVersionTagging"
|
||||
],
|
||||
"Resource": ["arn:aws:s3:::sast-memory/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Statement | Resource | In one line |
|
||||
| --- | --- | --- |
|
||||
| `BucketSettingsReadOnly` | `arn:aws:s3:::sast-memory` | The agent can list the bucket and its versions and *read* its location, versioning and lifecycle settings, which is what the startup check needs; it cannot change them. |
|
||||
| `ObjectsReadWrite` | `arn:aws:s3:::sast-memory/*` | The agent reads, writes and deletes objects and their versions and reads and writes object tags; this covers row objects, blobs, the index, the Select probe, and the `kind` tag the store filters by. |
|
||||
|
||||
So the agent writes objects freely but cannot change bucket settings: turning versioning off or
|
||||
removing the expiry rule needs the admin.
|
||||
|
||||
## The `.env` settings
|
||||
|
||||
The store reads its endpoint and credentials from `.env` in the working directory or from the
|
||||
environment (`minio_settings`), never from a manifest, and never prints them.
|
||||
|
||||
| Variable | Value for this server | Notes |
|
||||
| --- | --- | --- |
|
||||
| `OUSAST_MEMORY` | `minio://sast-memory` | `minio://<bucket>[/<prefix>]`; unset means the local `FileStore` |
|
||||
| `MINIO_ENDPOINT` | `files.because-security.com` | host[:port] without a scheme (the `minio` SDK's endpoint); required |
|
||||
| `MINIO_ACCESS_KEY` | the agent account's access key | required |
|
||||
| `MINIO_SECRET_KEY` | the agent account's secret key | required |
|
||||
| `MINIO_SECURE` | `true` | TLS; `0`, `false`, `no` or `off` turn it off; default `true` |
|
||||
| `MINIO_REGION` | optional | spares a GetBucketLocation call |
|
||||
|
||||
- **Never commit `.env`.** It is gitignored (`.gitignore`: `.env`, `.env.*`, except
|
||||
`.env.example`). `.env.example` lists these names with empty values.
|
||||
- **`.env` never overrides a variable already exported in your shell** (`config.load_dotenv`).
|
||||
A stale `export MINIO_SECRET_KEY=...` in your profile silently wins over the file; `unset` it
|
||||
when a change in `.env` seems to have no effect.
|
||||
- The store needs the `minio` extra (`uv sync --extra minio`); without it opening the store fails
|
||||
with `OUSAST_MEMORY=minio://... needs the minio SDK: install openultrasast[minio]`. A missing
|
||||
variable fails with `OUSAST_MEMORY=minio://... needs MINIO_ENDPOINT, MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY in .env or the environment` (naming only the missing ones).
|
||||
|
||||
## What the store verifies at startup
|
||||
|
||||
`MinioStore.verify_bucket()` runs once per process for each bucket and prefix. It reads the
|
||||
versioning status and the lifecycle rules, writes a two-line probe object at
|
||||
`_probe/select.jsonl` (under the prefix, if any), queries it with S3 Select and reads its tags.
|
||||
Every missing piece is collected into one error:
|
||||
|
||||
```text
|
||||
memory minio://sast-memory: the bucket is not set up for the store. The store never configures its bucket (an admin sets it once, the store verifies); missing:
|
||||
- <one line per missing piece>
|
||||
```
|
||||
|
||||
The lines, exactly as the code writes them for this bucket at the root (`<failure>` is the
|
||||
server's error code and the first 200 characters of its message):
|
||||
|
||||
| Check | Refusal line |
|
||||
| --- | --- |
|
||||
| versioning unreadable | `versioning cannot be read (<failure>): the agent needs s3:GetBucketVersioning on arn:aws:s3:::sast-memory, and versioning must be Enabled` |
|
||||
| versioning off | `versioning is Off, it must be Enabled (provenance cites object versions). Admin: aws s3api put-bucket-versioning --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory --versioning-configuration Status=Enabled` (the actual status, e.g. `Suspended`, replaces `Off` when set) |
|
||||
| lifecycle unreadable | `the lifecycle configuration cannot be read (<failure>): the agent needs s3:GetLifecycleConfiguration on arn:aws:s3:::sast-memory, and a rule must expire runs/` |
|
||||
| a rule expires everything | `lifecycle rule(s) <rule ids> expire the whole store (repos/, facts/ and the index would be deleted); limit them to runs/` |
|
||||
| no `runs/` rule | `no enabled lifecycle rule expires runs/ after a number of days. Admin (merged with any rules the bucket already has): aws s3api put-bucket-lifecycle-configuration --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'` |
|
||||
| tags unreadable | `object tags cannot be read on _probe/select.jsonl (<failure>): the store filters row objects by their kind tag, so the agent needs s3:GetObjectTagging on arn:aws:s3:::sast-memory/*` |
|
||||
| S3 Select missing | `S3 Select (SelectObjectContent over JSON Lines) does not answer on _probe/select.jsonl (<failure>). The store requires it and has no local fallback: use a server with S3 Select (RustFS has it; some MinIO releases removed it) and let the agent s3:PutObject and s3:GetObject` |
|
||||
|
||||
The admin commands inside the messages use `--endpoint-url "$MINIO_ENDPOINT"`; with the
|
||||
host-only `MINIO_ENDPOINT` above, pass the full URL (`https://files.because-security.com`) as in
|
||||
the steps on this page.
|
||||
|
||||
There is **no fallback**: the store does not start in a degraded mode, does not drop to a local
|
||||
store, and does not fetch and filter objects itself.
|
||||
|
||||
## The RustFS Select limit and the fixed-schema rule
|
||||
|
||||
RustFS's S3 Select infers an object's JSON schema from its leading rows. A `where` field that is
|
||||
absent from those rows is unbound, even if a later row has it: the query fails with
|
||||
`EvaluatorBindingDoesNotExist` instead of matching (measured 2026-10-02 with the field first
|
||||
present at row 5000). The store does not read that as "no rows", since that answer could silently
|
||||
drop matches; it raises:
|
||||
|
||||
```text
|
||||
memory minio://sast-memory: S3 Select on <key> failed (EvaluatorBindingDoesNotExist: ...) (a field of [<fields>] is absent from the object's leading rows, which the server reads as its schema); the store has no local fallback
|
||||
```
|
||||
|
||||
The rule that follows: a field the store filters on must be present in every row of its kind
|
||||
from the first row on, i.e. each row kind has a fixed set of queryable fields. Enforcing that set
|
||||
per kind in the store is in progress and not on main as of this page (see
|
||||
[Memory](memory.md#queryable-fields-per-kind-in-progress)).
|
||||
|
||||
## Checks against the real server
|
||||
|
||||
The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_MINIO=1` (bucket
|
||||
`OUSAST_MEMORY_TEST_BUCKET`, else `MINIO_BUCKET`). They verify the bucket at its root and write
|
||||
only under a fresh `contract-<id>/` prefix; they never configure the bucket. More on the host
|
||||
setup: [ax on this host](ops/ax/README.md).
|
||||
@@ -0,0 +1,125 @@
|
||||
# Scanning
|
||||
|
||||
`ousast scan PATH --mode quick|standard|deep` scans a checkout; `ousast pre-push` checks only
|
||||
what a push (or an explicit `--base`/`--head` pair) changes. The stage-by-stage pipeline,
|
||||
evidence ladder and score are in [Architecture](architecture.md); this page shows the flows.
|
||||
|
||||
## The three scan modes
|
||||
|
||||
The stages a mode requests are `MODE_STAGES` in `src/openultrasast/stages.py`: `quick` runs
|
||||
STATIC, `standard` adds MAP, `deep` adds REGRESS.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start["ousast scan PATH --mode M"] --> policy["policy_load, ruleset_load, policy_check"]
|
||||
policy --> prep["static_mapping, preprocess, entry_point_mapping, rank, calibrate"]
|
||||
prep --> which{"mode"}
|
||||
which -- "quick, deep" --> qf["quick_findings: language-scoped pattern rules"]
|
||||
which -- "standard" --> hp["hunter_pool: the same rules per ranked target"]
|
||||
qf --> verify["verify, record_calibration, score"]
|
||||
hp --> verify
|
||||
verify --> mapq{"MAP requested?"}
|
||||
mapq -- "quick: no" --> out["report.md, report.sarif, manifest.json"]
|
||||
mapq -- "standard, deep" --> map["MAP: complexity map, semantic overlay, obligations, Joern model layer"]
|
||||
map --> regq{"REGRESS requested?"}
|
||||
regq -- "standard: no" --> out
|
||||
regq -- "deep" --> reg["REGRESS: snippets in a Docker sandbox, verdicts.json"]
|
||||
reg --> out
|
||||
```
|
||||
|
||||
| Mode | Adds | Needs | Without it |
|
||||
| --- | --- | --- | --- |
|
||||
| `quick` | pattern rules, entry-point hints, ranking, verification, score | nothing | - |
|
||||
| `standard` | MAP: complexity map, semantic overlay, authorization obligations, the Joern model layer; `fusion` (deterministic) | Joern; a provider key for the residual LLM question | `cpg_unavailable` degradation; the LLM parts are skipped and recorded |
|
||||
| `deep` | REGRESS: promoted candidates loaded in a sandbox with no network, read-only source, non-root | a working `docker` | `sandbox_unavailable` degradation |
|
||||
|
||||
Sources: `README.md` (scan modes), `src/openultrasast/cli.py` (`run_stage` calls),
|
||||
[Architecture](architecture.md#the-scan-pipeline).
|
||||
|
||||
## Quick rules per language
|
||||
|
||||
Rules run only against files of their language. The bundled ruleset lives in
|
||||
`src/openultrasast/ruleset/<language>/rules.toml`.
|
||||
|
||||
| Language | Classes (CWE) |
|
||||
| --- | --- |
|
||||
| C / C++ | stack buffer overflow (121), format string (134), command injection (78) |
|
||||
| Python | command injection (78), code injection (95), SSTI (94), deserialization (502), SQLi (89), path traversal (22), SSRF (918), weak hash (327), insecure default (489) |
|
||||
| JavaScript / TS | command injection (78), code injection (95), XSS (79), SQLi (89), path traversal (22), SSRF (918), weak hash (327), deserialization (502) |
|
||||
| Java + Groovy templates | command injection (78), SQLi (89), weak hash (327), deserialization (502), unescaped template XSS (79) |
|
||||
| PHP | SQLi (89), command injection (78), file inclusion (98), path (22), open redirect (601), SSRF (918); code eval (95), unserialize (502) and echo XSS (79) ship as `shadow` rules |
|
||||
|
||||
The detection gate (`python -m openultrasast.gate`) enforces at least 90% recall and under 10%
|
||||
false positives per language on the bundled corpora in `benchmarks/manifests/`. Those corpora are
|
||||
cheat-sheet fixtures: they catch regressions, they do not estimate recall on real code. **PHP is
|
||||
not in the gate**; its numbers are in-sample development numbers
|
||||
(`benchmarks/measurements/2026-09-30-php-quick-rules/measurement.json`). See `README.md` for the
|
||||
gate's per-language output.
|
||||
|
||||
## The Joern engine
|
||||
|
||||
The model layer (`src/openultrasast/model/`, `src/openultrasast/cpg/`) is the core of `standard`.
|
||||
It builds one code property graph per scan with Joern (`joern-parse`, then batched
|
||||
`joern --script` queries from `cpg/queries/*.sc`) and sends each question to the arbiter of its
|
||||
type: **taint** (source to sink with no discharging sanitizer), **dominance** (does a guard govern
|
||||
the obligated operation) and **configuration** (constant abstraction over a value). Taint facts
|
||||
ship for C, Java, JavaScript, PHP and Python (`src/openultrasast/ruleset/semantic/*.toml`).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
src["Checkout"] --> parse["joern-parse: one CPG per repository"]
|
||||
parse --> q["Batched joern --script queries"]
|
||||
q --> arb{"Arbiter by question type"}
|
||||
arb --> taint["Taint: flow with no discharging sanitizer"]
|
||||
arb --> dom["Dominance: obligation with no guard"]
|
||||
arb --> conf["Configuration: permissive value"]
|
||||
taint --> rung["Rung: model_entailed or suspicion"]
|
||||
dom --> rung
|
||||
conf --> rung
|
||||
rung -- "residual question only" --> judge["LLM judge: confirm or contradict, never create a flow"]
|
||||
```
|
||||
|
||||
The Docker image ships Joern and `php-cli` (which the PHP frontend needs); without Joern the
|
||||
layer records `cpg_unavailable` and the scan is otherwise unchanged. Engine installation and the
|
||||
Joern features used are in [Engine and pre-push](ops/README.md).
|
||||
|
||||
## Framework priors
|
||||
|
||||
Rule and taint-fact entries that know a framework or library carry a `framework` or `library` tag
|
||||
naming a row of `src/openultrasast/ruleset/frameworks.toml` (for example `wordpress`, `flask`,
|
||||
`django`, `express`, `spring`). The loaders take `priors=`: `"all"` (the default and today's
|
||||
behaviour), `"off"` (language-level knowledge only) or a set of ids, so the contribution of
|
||||
framework knowledge can be measured and switched off. The decision engine's first slice ran with
|
||||
priors off ([Decision engine](decision-engine.md#first-measured-slice-2026-10-01)).
|
||||
|
||||
## Pre-push: the delta check (experimental)
|
||||
|
||||
`ousast pre-push` analyses the commits a `git push` would publish, or an explicit
|
||||
`--base`/`--head` pair, and compares head with base so that only new or worsened defects are
|
||||
candidates. Sources: `ousast pre-push --help`, `src/openultrasast/push/`,
|
||||
[Engine and pre-push](ops/README.md#explicit-local-replay).
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
input["git push refs on stdin, or --base / --head"] --> resolve["Resolve both commit identities"]
|
||||
resolve --> snap["Materialise tracked bytes in private scratch storage"]
|
||||
snap --> regions["Discover changed regions, up to --max-regions"]
|
||||
regions --> engine["Evidence ranker and arbiter on head"]
|
||||
engine --> cmp["Targeted base comparison: new or worsened only"]
|
||||
cmp --> admit{"Admission policy: capability registry"}
|
||||
admit -- "registry empty today" --> diag["Diagnostic candidates in the artifact"]
|
||||
admit -- "an enabled capability" --> alert["Normal alert"]
|
||||
diag --> art["JSON artifact outside the repository"]
|
||||
alert --> art
|
||||
art --> exit{"--mode"}
|
||||
exit -- "advisory, the default" --> allow["Exit 0: allow"]
|
||||
exit -- "blocking, explicit opt-in" --> block["May block the push"]
|
||||
```
|
||||
|
||||
- `--deadline` (30 s by default) bounds preparation, both revisions and reporting together;
|
||||
`--incomplete-coverage block` opts into failing when coverage is incomplete.
|
||||
- The default capability registry is empty pending independent qualification, so no normal alert
|
||||
is emitted today; every candidate stays diagnostic.
|
||||
- A model is called only with an explicit `--model-config`.
|
||||
- Hook installation (`ops/install-pre-push`) and the private cache (`--cache-dir`) are documented
|
||||
in [Engine and pre-push](ops/README.md#experimental-pre-push-integration).
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
site_name: OpenUltraSAST
|
||||
site_description: An experimental static security analyser and repository safety net, with an agentic plane and a learned decision engine in development.
|
||||
repo_url: https://github.com/norandom/OpenUltraSAST
|
||||
edit_uri: edit/main/docs/
|
||||
docs_dir: docs
|
||||
site_dir: site
|
||||
strict: true
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
- content.code.copy
|
||||
- search.highlight
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Light mode
|
||||
|
||||
validation:
|
||||
omitted_files: warn
|
||||
absolute_links: warn
|
||||
unrecognized_links: warn
|
||||
anchors: warn
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- tables
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.details
|
||||
- pymdownx.snippets:
|
||||
base_path: ["."]
|
||||
check_paths: true
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
|
||||
nav:
|
||||
- Overview: index.md
|
||||
- How it works:
|
||||
- Scanning: scanning.md
|
||||
- Architecture: architecture.md
|
||||
- The plane on ax: plane.md
|
||||
- Memory: memory.md
|
||||
- Memory and detection: memory-and-detection.md
|
||||
- Decision engine: decision-engine.md
|
||||
- Evaluation: evaluation.md
|
||||
- Using it:
|
||||
- Examples: examples.md
|
||||
- Threat model: threat-model.md
|
||||
- Operations (maintainers):
|
||||
- Engine and pre-push: ops/README.md
|
||||
- ax on this host: ops/ax/README.md
|
||||
- RustFS setup for the memory store: rustfs.md
|
||||
@@ -11,6 +11,11 @@ requires-python = ">=3.11"
|
||||
dependencies = ["pyyaml>=6.0"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
# The documentation site (mkdocs.yml, docs/). Mermaid renders client-side through Material's superfences fence.
|
||||
docs = [
|
||||
"mkdocs>=1.6,<2",
|
||||
"mkdocs-material>=9.5,<10",
|
||||
]
|
||||
# The plane's memory store on MinIO (OUSAST_MEMORY=minio://<bucket>, plane/memory.py). Imported lazily; the
|
||||
# core install and the file store need nothing beyond PyYAML.
|
||||
minio = [
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""The documentation site builds strictly, its Mermaid diagrams are well formed, and the RustFS page is complete.
|
||||
|
||||
The strict build runs only where ``mkdocs`` is importable (the ``docs`` extra); the other checks read the Markdown
|
||||
and always run. The Mermaid check is syntactic (fences, a known diagram type, balanced brackets and quotes, the node
|
||||
budget): the site renders diagrams in the browser, so nothing here can run the real parser.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS = ROOT / "docs"
|
||||
PAGES = sorted(DOCS.rglob("*.md"))
|
||||
DIAGRAM_TYPES = ("flowchart", "graph", "sequenceDiagram", "stateDiagram-v2", "stateDiagram", "classDiagram", "erDiagram")
|
||||
MAX_NODES = 15
|
||||
FENCE = re.compile(r"^(\s*)(`{3,}|~{3,})(.*)$")
|
||||
|
||||
BUCKET_ACTIONS = (
|
||||
"s3:ListBucket", "s3:ListBucketVersions", "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:GetLifecycleConfiguration",
|
||||
) # fmt: skip
|
||||
OBJECT_ACTIONS = (
|
||||
"s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:DeleteObject", "s3:DeleteObjectVersion",
|
||||
"s3:GetObjectTagging", "s3:PutObjectTagging", "s3:GetObjectVersionTagging",
|
||||
) # fmt: skip
|
||||
SECRET_SHAPES = (
|
||||
re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"), # AWS-style access key id
|
||||
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
re.compile(r"\b(?:sk|pk)-[A-Za-z0-9_-]{16,}"), # provider API keys
|
||||
re.compile(r"(?:SECRET|ACCESS)_KEY\s*=\s*[^\s<`.]"), # an assignment with a value
|
||||
re.compile(r"(?<![0-9a-f])[A-Za-z0-9+/]{40}(?![A-Za-z0-9+/])"), # a 40-char secret-access-key shape
|
||||
)
|
||||
|
||||
|
||||
def mermaid_blocks(text: str, where: str) -> list[str]:
|
||||
"""The bodies of the ```mermaid fences. Every fence must close with the same marker (balanced fences)."""
|
||||
blocks: list[str] = []
|
||||
opener: str | None = None # the open fence's marker, None outside a fence
|
||||
language, body = "", []
|
||||
for line in text.splitlines():
|
||||
match = FENCE.match(line)
|
||||
if opener is None:
|
||||
if match:
|
||||
opener, language, body = match.group(2), match.group(3).strip(), []
|
||||
continue
|
||||
if match and match.group(2).startswith(opener) and not match.group(3).strip():
|
||||
if language == "mermaid":
|
||||
blocks.append("\n".join(body))
|
||||
opener = None
|
||||
continue
|
||||
body.append(line)
|
||||
assert opener is None, f"{where}: a {opener} fence is never closed"
|
||||
return blocks
|
||||
|
||||
|
||||
def _balanced(line: str) -> bool:
|
||||
outside = re.sub(r'"[^"]*"', '""', line)
|
||||
if outside.count('"') % 2:
|
||||
return False
|
||||
stack: list[str] = []
|
||||
for char in outside:
|
||||
if char in "[({":
|
||||
stack.append(char)
|
||||
elif char in "])}" and (not stack or "[({"["])}".index(char)] != stack.pop()):
|
||||
return False
|
||||
return not stack
|
||||
|
||||
|
||||
def _nodes(block: str) -> set[str]:
|
||||
"""Node ids of a flowchart: the leading identifier of every edge endpoint, labels and shapes removed."""
|
||||
nodes: set[str] = set()
|
||||
for raw in block.splitlines()[1:]:
|
||||
line = re.sub(r'"[^"]*"', "", raw).strip()
|
||||
if not line or line.startswith(("%%", "subgraph", "end", "classDef", "class ", "style ", "direction")):
|
||||
continue
|
||||
line = re.sub(r"\[[^\]]*\]|\([^)]*\)|\{[^}]*\}", "", line)
|
||||
for part in re.split(r"-\.->|-->|==>|---|--|&", line):
|
||||
match = re.match(r"\s*([A-Za-z_]\w*)", part)
|
||||
if match:
|
||||
nodes.add(match.group(1))
|
||||
return nodes
|
||||
|
||||
|
||||
def test_every_page_has_well_formed_mermaid() -> None:
|
||||
assert PAGES, "docs/ has no pages"
|
||||
total = 0
|
||||
for page in PAGES:
|
||||
for block in mermaid_blocks(page.read_text(encoding="utf-8"), page.name):
|
||||
total += 1
|
||||
lines = [ln for ln in block.splitlines() if ln.strip() and not ln.strip().startswith("%%")]
|
||||
assert lines, f"{page.name}: empty mermaid block"
|
||||
kind = lines[0].split()[0]
|
||||
assert kind in DIAGRAM_TYPES, f"{page.name}: unknown diagram type {kind!r}"
|
||||
for line in lines[1:]:
|
||||
assert _balanced(line), f"{page.name}: unbalanced brackets or quotes in {line.strip()!r}"
|
||||
if kind == "sequenceDiagram":
|
||||
assert ";" not in line, f"{page.name}: ';' ends a sequence-diagram statement: {line.strip()!r}"
|
||||
if kind in ("flowchart", "graph"):
|
||||
found = _nodes("\n".join(lines))
|
||||
assert len(found) <= MAX_NODES, f"{page.name}: {len(found)} nodes > {MAX_NODES}: split the diagram"
|
||||
assert total >= 10, f"only {total} mermaid diagrams in docs/"
|
||||
|
||||
|
||||
def test_mermaid_fence_is_configured() -> None:
|
||||
config = (ROOT / "mkdocs.yml").read_text(encoding="utf-8")
|
||||
assert "pymdownx.superfences" in config and "name: mermaid" in config and "fence_code_format" in config
|
||||
|
||||
|
||||
def test_rustfs_page_lists_the_complete_policy_and_no_secret() -> None:
|
||||
text = (DOCS / "rustfs.md").read_text(encoding="utf-8")
|
||||
for action in BUCKET_ACTIONS + OBJECT_ACTIONS:
|
||||
assert f'"{action}"' in text, f"rustfs.md: the policy lacks {action}"
|
||||
for needle in (
|
||||
'"Sid": "BucketSettingsReadOnly"', '"Sid": "ObjectsReadWrite"', '"arn:aws:s3:::sast-memory"', '"arn:aws:s3:::sast-memory/*"',
|
||||
"put-bucket-versioning", "put-bucket-lifecycle-configuration", "--endpoint-url https://files.because-security.com",
|
||||
"--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "minio://sast-memory", "MINIO_ENDPOINT",
|
||||
"MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", "MINIO_SECURE", "MINIO_REGION", "EvaluatorBindingDoesNotExist",
|
||||
): # fmt: skip
|
||||
assert needle in text, f"rustfs.md lacks {needle!r}"
|
||||
for shape in SECRET_SHAPES:
|
||||
hit = shape.search(text)
|
||||
assert hit is None, f"rustfs.md has a secret-looking string near {hit.group(0)[:6]!r}..."
|
||||
|
||||
|
||||
def test_rustfs_page_quotes_the_refusals_verify_bucket_writes() -> None:
|
||||
"""Each refusal line the page quotes still appears, as a template fragment, in the store's code."""
|
||||
code = (ROOT / "src" / "openultrasast" / "plane" / "memory.py").read_text(encoding="utf-8")
|
||||
for fragment in (
|
||||
"the bucket is not set up for the store. The store never configures its ",
|
||||
"it must be Enabled (provenance cites object versions)",
|
||||
"expire the whole store (repos/, facts/ and the index would be ",
|
||||
"no enabled lifecycle rule expires",
|
||||
"the store filters row objects by ",
|
||||
"The store requires it and has no local fallback",
|
||||
"is absent from the object's leading rows, which the server reads as its schema",
|
||||
):
|
||||
assert fragment in code, f"memory.py no longer writes {fragment!r}: update docs/rustfs.md"
|
||||
|
||||
|
||||
@pytest.mark.skipif(importlib.util.find_spec("mkdocs") is None, reason="mkdocs is not installed (the docs extra)")
|
||||
def test_site_builds_strictly(tmp_path: Path) -> None:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-m", "mkdocs", "build", "--strict", "-f", str(ROOT / "mkdocs.yml"), "-d", str(tmp_path / "site")],
|
||||
cwd=ROOT, capture_output=True, text=True, timeout=300,
|
||||
) # fmt: skip
|
||||
assert proc.returncode == 0, f"mkdocs build --strict exited {proc.returncode}:\n{proc.stderr[-3000:]}"
|
||||
pages = list((tmp_path / "site").rglob("*.html"))
|
||||
assert len(pages) >= 12, f"the build wrote only {len(pages)} pages" # proves the build read the docs
|
||||
ax = (tmp_path / "site" / "ops" / "ax" / "index.html").read_text(encoding="utf-8")
|
||||
assert "Bring-up" in ax, "the ax operations page did not include ops/ax/README.md"
|
||||
leaked = [p.name for p in pages if ".kiro" in p.read_text(encoding="utf-8") or "kiro-" in p.read_text(encoding="utf-8")]
|
||||
assert not leaked, f"maintainer Kiro tooling reached the site: {leaked}"
|
||||
@@ -132,6 +132,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "2.2.1"
|
||||
@@ -141,6 +150,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backrefs"
|
||||
version = "8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beartype"
|
||||
version = "0.22.9"
|
||||
@@ -340,6 +362,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -403,6 +434,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghp-import"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.75.0"
|
||||
@@ -508,6 +551,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librt"
|
||||
version = "0.11.0"
|
||||
@@ -581,6 +636,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/4f/700155c8c20d9e655dd0732b5fc3c7614f291b9148da271d7388e50bf774/markdown-3.11.tar.gz", hash = "sha256:180224db6aed87ba9ce1f2781ebcd5826253de8ff637112090e24b84502bbf9f", size = 485623, upload-time = "2026-09-25T13:46:23.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/1e/32971905a7ab47f8b66866ed949fa48b104ba1c4a6fa57794c4f2c4b2cb8/markdown-3.11-py3-none-any.whl", hash = "sha256:cd6c89e7eb308c8b332ed673215a52d208a43f8bacc030b1419376129408719e", size = 111296, upload-time = "2026-09-25T13:46:22.163Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
@@ -593,6 +657,80 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
@@ -602,6 +740,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mergedeep"
|
||||
version = "1.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minio"
|
||||
version = "7.2.20"
|
||||
@@ -618,6 +765,75 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs"
|
||||
version = "1.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "ghp-import" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "mergedeep" },
|
||||
{ name = "mkdocs-get-deps" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "pyyaml-env-tag" },
|
||||
{ name = "watchdog" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-get-deps"
|
||||
version = "0.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mergedeep" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.7.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
{ name = "backrefs" },
|
||||
{ name = "colorama" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown" },
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-material-extensions" },
|
||||
{ name = "paginate" },
|
||||
{ name = "pygments" },
|
||||
{ name = "pymdown-extensions" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material-extensions"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
@@ -923,6 +1139,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
docs = [
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-material" },
|
||||
]
|
||||
minio = [
|
||||
{ name = "minio" },
|
||||
]
|
||||
@@ -948,6 +1168,8 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "minio", marker = "extra == 'minio'", specifier = ">=7.2,<8" },
|
||||
{ name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6,<2" },
|
||||
{ name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5,<10" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "tree-sitter", marker = "extra == 'semantic'", specifier = ">=0.23,<0.27" },
|
||||
{ name = "tree-sitter-c", marker = "extra == 'semantic'", specifier = ">=0.23" },
|
||||
@@ -957,7 +1179,7 @@ requires-dist = [
|
||||
{ name = "tree-sitter-python", marker = "extra == 'semantic'", specifier = ">=0.23" },
|
||||
{ name = "tree-sitter-typescript", marker = "extra == 'semantic'", specifier = ">=0.23" },
|
||||
]
|
||||
provides-extras = ["minio", "semantic"]
|
||||
provides-extras = ["docs", "minio", "semantic"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -977,6 +1199,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paginate"
|
||||
version = "0.5.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.1.1"
|
||||
@@ -1194,6 +1425,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/94/858e0163cb4d83d6d8234018a01792c749a56daee41794cac7d6c46661e8/pymdown_extensions-12.1.tar.gz", hash = "sha256:fdb8f47f5d7fd069d10ef2a7d8908e613d7865ff0419d544a0ffd3984e884f11", size = 868245, upload-time = "2026-09-23T00:08:42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d1/98313da89960a604402266a115311b510254900ff1295ea426403f9423cc/pymdown_extensions-12.1-py3-none-any.whl", hash = "sha256:4a254b771acfcddc6c110a7f4590d818f1f849e2d5aacf69b4b07c8dd2a9700d", size = 277069, upload-time = "2026-09-23T00:08:40.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.0"
|
||||
@@ -1210,6 +1454,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-discovery"
|
||||
version = "1.4.2"
|
||||
@@ -1278,6 +1534,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml-env-tag"
|
||||
version = "1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
@@ -1331,6 +1599,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.26.0"
|
||||
@@ -1497,6 +1774,33 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "2.2.1"
|
||||
|
||||
Reference in New Issue
Block a user