argus-cortex 0.4.7__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.cursor/rules/agent-coding-packs.mdc +5 -1
  2. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.github/workflows/release.yml +1 -1
  3. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.gitignore +2 -0
  4. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/AGENTS.md +5 -1
  5. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/CHANGELOG.md +96 -1
  6. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/CLAUDE.md +8 -4
  7. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/Makefile +2 -2
  8. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/PKG-INFO +47 -8
  9. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/README.md +43 -7
  10. argus_cortex-0.6.0/SUITE.md +106 -0
  11. argus_cortex-0.6.0/consent-schema.json +257 -0
  12. argus_cortex-0.6.0/gpu-lease-schema.json +476 -0
  13. argus_cortex-0.6.0/gpu-tenant-schema.json +369 -0
  14. argus_cortex-0.6.0/provenance-schema.json +125 -0
  15. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/pyproject.toml +7 -0
  16. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/__init__.py +36 -0
  17. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/_version.py +2 -2
  18. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/backends.py +24 -12
  19. argus_cortex-0.6.0/src/argus_cortex/consent.py +240 -0
  20. argus_cortex-0.6.0/src/argus_cortex/gpu.py +449 -0
  21. argus_cortex-0.6.0/src/argus_cortex/gpu_broker.py +577 -0
  22. argus_cortex-0.6.0/src/argus_cortex/gpu_wire.py +368 -0
  23. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/moderation/policies.py +10 -2
  24. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/moderation/singguard.py +21 -10
  25. argus_cortex-0.6.0/src/argus_cortex/provenance.py +128 -0
  26. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/server.py +40 -7
  27. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/blob.py +7 -3
  28. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/lineage.py +12 -2
  29. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/schema.py +34 -0
  30. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/vector.py +30 -16
  31. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/xmp.py +92 -18
  32. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_backends.py +56 -0
  33. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_blob.py +8 -0
  34. argus_cortex-0.6.0/tests/test_consent.py +249 -0
  35. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_gpu.py +31 -0
  36. argus_cortex-0.6.0/tests/test_gpu_broker.py +534 -0
  37. argus_cortex-0.6.0/tests/test_gpu_wire.py +193 -0
  38. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_moderation.py +10 -0
  39. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_policies.py +3 -1
  40. argus_cortex-0.6.0/tests/test_provenance.py +144 -0
  41. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_server.py +15 -1
  42. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_store.py +13 -2
  43. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_store_pg_integration.py +14 -1
  44. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_vector.py +8 -0
  45. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_xmp.py +86 -0
  46. argus_cortex-0.4.7/SUITE.md +0 -98
  47. argus_cortex-0.4.7/src/argus_cortex/gpu.py +0 -242
  48. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.copier-answers.yml +0 -0
  49. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.env.example +0 -0
  50. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/.github/workflows/ci.yml +0 -0
  51. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/LICENSE +0 -0
  52. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/moderation/__init__.py +0 -0
  53. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/moderation/_http.py +0 -0
  54. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/moderation/nudenet.py +0 -0
  55. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/py.typed +0 -0
  56. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/__init__.py +0 -0
  57. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/config.py +0 -0
  58. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/errors.py +0 -0
  59. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/store/models.py +0 -0
  60. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/taxonomy.py +0 -0
  61. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/src/argus_cortex/wire.py +0 -0
  62. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/taxonomy-schema.json +0 -0
  63. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_smoke.py +0 -0
  64. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_taxonomy.py +0 -0
  65. {argus_cortex-0.4.7 → argus_cortex-0.6.0}/tests/test_wire.py +0 -0
@@ -17,13 +17,17 @@ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in th
17
17
  CLI equivalent: `ai-audit packs --bundle --repo argus-cortex`.
18
18
  2. **Claim** the specialist pack you pick:
19
19
  `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
20
- (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
20
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`; the token is in your environment, set for Claude Code
21
+ in `~/.claude/settings.json`. If it is empty or you get a 401, ask the user; don't search env files).
21
22
  3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
22
23
  HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
23
24
  or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
24
25
  4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-cortex","patch":"<diff>","pack_id":"<id>"}`
25
26
  — a passing run marks the pack `resolved`.
26
27
 
28
+ **Search** before crawling: `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/search?q=<question>&repo=argus-cortex&kind=wiki`
29
+ (wiki sections and packs with links; CLI `ai-audit search "<question>" --repo argus-cortex`).
30
+
27
31
  World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
28
32
  Full flow: ai-code-auditor `AGENTS.md`.
29
33
  <!-- ai-audit:agent-packs:end -->
@@ -159,7 +159,7 @@ jobs:
159
159
  exit 1
160
160
  fi
161
161
  failed=0
162
- for repo in argus-curator argus-lens argus-proof argus-forge argus-darkroom; do
162
+ for repo in argus-curator argus-lens argus-proof argus-forge argus-darkroom argus-mint; do
163
163
  if gh api "repos/${GITHUB_REPOSITORY_OWNER}/${repo}/dispatches" \
164
164
  -f event_type=cortex-released -f "client_payload[version]=${tag#v}"; then
165
165
  echo "Dispatched cortex-released to $repo"
@@ -10,3 +10,5 @@ build/
10
10
  .env
11
11
  # hatch-vcs writes this at build time (version derived from git tags)
12
12
  src/argus_cortex/_version.py
13
+ # generated GitHub social previews (brand/tools/panoptes.py social)
14
+ brand/social/
@@ -12,13 +12,17 @@ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in th
12
12
  CLI equivalent: `ai-audit packs --bundle --repo argus-cortex`.
13
13
  2. **Claim** the specialist pack you pick:
14
14
  `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
15
- (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
15
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`; the token is in your environment, set for Claude Code
16
+ in `~/.claude/settings.json`. If it is empty or you get a 401, ask the user; don't search env files).
16
17
  3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
17
18
  HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
18
19
  or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
19
20
  4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-cortex","patch":"<diff>","pack_id":"<id>"}`
20
21
  — a passing run marks the pack `resolved`.
21
22
 
23
+ **Search** before crawling: `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/search?q=<question>&repo=argus-cortex&kind=wiki`
24
+ (wiki sections and packs with links; CLI `ai-audit search "<question>" --repo argus-cortex`).
25
+
22
26
  World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
23
27
  Full flow: ai-code-auditor `AGENTS.md`.
24
28
  <!-- ai-audit:agent-packs:end -->
@@ -7,6 +7,99 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-11-15
11
+
12
+ ### Added
13
+ - **GPU broker client (`argus_cortex.gpu_broker`).** `BrokerCoordinator` asks argus-iris, the suite's GPU broker,
14
+ for a lease; iris places it on a node, queues it by priority, reserves the VRAM and evicts idle models to make
15
+ room. Select it with `ARGUS_GPU_COORDINATOR=broker` and `ARGUS_GPU_BROKER_URL` (optional
16
+ `ARGUS_GPU_BROKER_TOKEN`, `ARGUS_GPU_TENANT`, `ARGUS_GPU_TENANT_MANIFEST`). A lease long-polls while queued,
17
+ heartbeats in the background, re-attaches if the broker restarts, and reports its outcome (`ok`, `failed`,
18
+ `oom`, `cancelled`) and peak VRAM on release. Each request carries a `request_id`, so the one retry after a
19
+ lost response gets the same lease. It fails open: if the broker is unreachable or answers 5xx, the lease comes
20
+ from `ARGUS_GPU_BROKER_FALLBACK` (`lease`, the default, or `none`). A 4xx or an incompatible version raises
21
+ `BrokerError`; a refusal raises the new `GpuLeaseRefused`. httpx is behind `[remote]` (#78).
22
+ - **GPU wire models (`argus_cortex.gpu_wire`).** The lease contract (`LeaseRequest`, `LeaseStatus`, `Heartbeat`,
23
+ `HeartbeatReply`, `LeaseRelease`; `lease_version` 1.0) and `TenantManifest` (`tenant_version` 1.0), the
24
+ `gpu-tenant.yaml` every repo that uses a GPU commits: its kinds of work with target, footprint (`vram_mb`, and
25
+ `model_mb` for the weights that stay loaded), priority and preemptibility, and its resident models with the
26
+ routes and hints the broker uses to reach, unload, evict and warm them. Committed `gpu-lease-schema.json` and
27
+ `gpu-tenant-schema.json` (#78).
28
+ - **`lease()` options and handle.** Every coordinator's `lease()` takes optional `priority`, `target`, `kind`,
29
+ `preemptible`, `constraints` and `model_mb`, which only the broker uses, and yields a `GpuLease` (`node`,
30
+ `base_url`, `should_yield()`, `report_peak_vram()`). Every coordinator gains an async `alease()`. Existing
31
+ `with coordinator.lease(caller=, min_vram_mb=):` calls are unchanged (#78).
32
+ - **`[yaml]` extra** (PyYAML) for `load_tenant_manifest()` reading YAML; JSON needs nothing (#78).
33
+ - `SUITE.md`: a row for argus-handbook, the suite's public docs at docs.dragonhound.dev.
34
+ - `brand/`: the suite's identity (vesica-iris suite mark, one mark per package, Panoptes lettering font, the
35
+ identity sheet, and `brand/tools/panoptes.py` to regenerate them). Panoptes 0.2 adds capitals, digits and basic
36
+ punctuation (85 glyphs); glyphs made only of dots, like the colon, keep every dot in the font build. Favicon sets
37
+ for every web UI (`mark/favicon/`, `packages/<pkg>/favicon/`) and `panoptes.py favicons` to regenerate them;
38
+ `brand/README.md` says where each UI puts them. Not part of the package: excluded from the wheel and the sdist.
39
+
40
+ ### Changed
41
+ - `SUITE.md`: the "Share the GPU" row points at the broker and asks each repo to declare its GPU use in a
42
+ `gpu-tenant.yaml` (#78). argus-studio's row says its `/docs` keeps only the captioning concept pages; the setup
43
+ guides and package docs moved to argus-handbook, and the old paths 301 there.
44
+ - `release.yml` also sends `cortex-released` to argus-mint, whose image installs cortex from its `server` extra's
45
+ range and now rebuilds on it (argus-mint#29).
46
+ - **Behaviour changes a stage may notice on this bump (#73):**
47
+ - `cross_site_refuse()` no longer trusts a same-host write by bare hostname (DNS rebinding). A same-host
48
+ `Origin` passes only when the host is an IP literal, `localhost`, or listed in the new `trusted_hosts`
49
+ argument; a server reached by a DNS name must pass it (or list the origin in `trusted_origins`).
50
+ - `RemoteBackend.post_json()` retries only failures where the request never reached the server (connect
51
+ errors, pool timeouts), not a `ReadTimeout`; pass `idempotent=True` to keep the old retries.
52
+ - `policies.mode()` raises `ModerationConfigError` for an unknown policy name instead of returning `nsfw`.
53
+ - `QdrantVectorStore.search()` returns the key passed to `upsert()` as `VectorHit.id`, not its UUID5; the
54
+ original key rides in the reserved `_point_id` payload field (`POINT_ID_FIELD`), stripped from `payload`.
55
+ - `SingGuardClient` refuses a multi-frame image (animated GIF/WebP/APNG, multi-page TIFF) with
56
+ `GuardCheckFailed` instead of judging frame 0.
57
+ - `CORTEX_S3_ENDPOINT` refuses a scheme other than `http://`/`https://`.
58
+
59
+ ### Fixed
60
+ - `LeaseCoordinator` refuses a symlinked or non-regular lock file and chmods it only when it owns it (#73).
61
+ - The lineage schema refuses a dataset membership whose `edit_id` belongs to another caption (a trigger, so it
62
+ reaches existing tables) (#73).
63
+ - XMP: regions are written as valid RDF (no `parseType` on attribute-form structs) and read back from nested
64
+ `rdf:Description` forms; a sidecar's prefixes no longer rebind the process-wide ElementTree registry; concurrent
65
+ `write_xmp()` merges into one directory take an exclusive lock so neither drops the other's properties (#73).
66
+ - `ensure_schema()` serialises on a Postgres advisory lock and creates the membership trigger in one statement, so
67
+ several stages starting against one database no longer fail with "tuple concurrently updated" or "trigger
68
+ already exists".
69
+ - `BrokerCoordinator` gives up a queued request whenever the wait ends without a grant (a 5xx or a dropped
70
+ connection mid-poll, a bad payload, an interrupt), not only on timeout or cancellation, so the broker never grants
71
+ an orphaned lease while the work runs on the fallback.
72
+ - `LeaseCoordinator.alease()` waits on its own thread instead of the loop's default executor, so more concurrent
73
+ waiters than executor threads can no longer starve the holder's release.
74
+ - Docs: `slow` and `cascade` are supported by agent-composer's guard router as well as argus-cull's `singguard`
75
+ service, not by argus-cull's alone (`SingGuardClient` docstrings, README). Code is unchanged.
76
+
77
+ ## [0.5.0] - 2026-11-14
78
+
79
+ ### Added
80
+ - **`argus_cortex.provenance`.** `Provenance`, `ProvenanceKind` and `CLEARED_KINDS`, hoisted from
81
+ argus-mask so every package that stores where a face, voice or image came from reads one shape,
82
+ with a committed `provenance-schema.json` (#35).
83
+ - **`argus_cortex.consent`.** `ConsentRecord`, a real person's likeness release as argus-mask#26
84
+ defined it, keyed by the `release_sha256` a `consented` `Provenance` names. `consent_valid()`
85
+ returns a `ConsentVerdict` with one of a closed set of refusal codes, checked in a fixed order
86
+ (`revoked`, `not-yet-signed`, `expired`, `likeness-not-covered`, `use-not-covered`,
87
+ `delivery-not-covered`), so every stage refuses for the same reasons. `consent_matches()` checks
88
+ that a record is the release and subject a provenance names. Committed `consent-schema.json` (#53).
89
+ A record is frozen and its sequences are tuples, so it cannot be changed after its checks pass.
90
+ It is refused when the age check names nobody or comes after signing, when a revocation is
91
+ requested before signing, or when a listed voice delivery is blank (#56).
92
+ - **`Provenance.generator`.** An optional `model`, `model_sha256`, `seed` and `prompt` for how a
93
+ generated source was made, allowed only on `synthetic` and `personal` sources (#53).
94
+
95
+ ### Changed
96
+ - **`Provenance` is strict, as argus-mask's is (argus-mask#64).** Unknown fields are refused instead
97
+ of dropped (`additionalProperties: false` in the schema). `licence` is read as `license`; both given
98
+ and different is refused. A `source` of only whitespace is refused. The derived `cleared` in a dumped
99
+ record is dropped on read and recomputed.
100
+ - **README:** lists the `[images]` and `[gpu]` extras, `ARGUS_GPU_DEVICE`, and the moderation policy
101
+ names with their illegal-rule letters (#42).
102
+
10
103
  ## [0.4.7] - 2026-11-13
11
104
 
12
105
  ### Added
@@ -229,7 +322,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
229
322
  (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
230
323
  retrying only transient errors, `resolve_device`).
231
324
 
232
- [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.7...HEAD
325
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.6.0...HEAD
326
+ [0.6.0]: https://github.com/smk762/argus-cortex/compare/v0.5.0...v0.6.0
327
+ [0.5.0]: https://github.com/smk762/argus-cortex/compare/v0.4.7...v0.5.0
233
328
  [0.4.7]: https://github.com/smk762/argus-cortex/compare/v0.4.6...v0.4.7
234
329
  [0.4.6]: https://github.com/smk762/argus-cortex/compare/v0.4.5...v0.4.6
235
330
  [0.4.5]: https://github.com/smk762/argus-cortex/compare/v0.4.4...v0.4.5
@@ -23,15 +23,19 @@ their own rows land here. Keep it short, because it is loaded into every session
23
23
  `src/argus_cortex/` (top level is pydantic-only; everything heavier is behind an extra and lazily imported):
24
24
 
25
25
  - `taxonomy.py` — the "moat" types every stage inherits verbatim: `TargetProfile`, `TargetStyle`/`TargetCategory` literals, and `taxonomy_schema()` (the source of the committed `taxonomy-schema.json`). Keep it dependency-light; a change here retrains everyone's expectations.
26
+ - `provenance.py` — `Provenance`/`ProvenanceKind`/`CLEARED_KINDS` for face, voice and image sources, hoisted from argus-mask (#19/#57): `personal` (never cleared) vs cleared `stock`/`synthetic`/`consented` (licence required; `consented` needs `subject` + `release_sha256`); `curated` reserved. **argus-mask owns the meaning of each field — change it only in step with mask.** Optional `generator` (model, sha256, seed, prompt) on `synthetic`/`personal` only. `provenance_schema()` renders the committed `provenance-schema.json` (`make schema`; drift-tested).
27
+ - `consent.py` — `ConsentRecord` (keyed by `release_sha256`, the join to a `consented` `Provenance`), `consent_valid()` → `ConsentVerdict` with a closed `ConsentRefusal` reason (checks in a fixed order; the first failure wins), `consent_matches()`, and `consent_schema()` → the committed `consent-schema.json`. Defined in argus-mask#26; **mask owns the meaning — change it only in step with mask.** `consent_valid` takes `at` from the caller (tz-aware, compared as a UTC day) and never reads the clock; an unknown use is refused, not raised. A record names a person: services return verdicts, never records.
26
28
  - `wire.py` — the versioned wire-schema toolkit consumers build their contracts on: `make_versioned_base(field_name, current, majors)` (a Pydantic base that stamps + version-checks a field like `proof_version`/`manifest_version`), `check_version()` (the major-compatibility gate), `render_schema()`/`wire_schema()` (the canonical committed-schema string behind each consumer's `schema --check` CLI), and `VersionError`.
27
29
  - `backends.py` — the AI-backend contract: `Backend`/`LocalBackend`/`RemoteBackend` (point at a hosted service via `from_host(host, port)` or `from_provider(RemoteProvider.…)`), plus `with_retries()` and `resolve_device()`. httpx is lazy, behind `[remote]`.
28
30
  - `server.py` — the shared FastAPI/ASGI scaffolding other repos' micro-servers mount: `WriteGuard` (pure-ASGI method gate), the `constant_refuse()` (read-only/replay) and `cross_site_refuse()` (cross-origin write) predicates, `env_flag()`, and the `UNSAFE_METHODS`/`TRUTHY`/`FALSY` constants. Starlette is behind `[server]`.
29
- - `gpu.py` — GPU tenancy (from argus-lens): the `GpuCoordinator` protocol (`lease(caller=, min_vram_mb=)`), `NullCoordinator`, `LeaseCoordinator` (host-wide `flock` **then** a wait for NVML free VRAM, so tenants outside the lock like ComfyUI and Ollama are accounted for), `free_vram_mb()`, `coordinator_from_env()` (`ARGUS_GPU_*` — host-wide names on purpose: every process must agree). NVML lazily, behind `[gpu]`; a probe that cannot run grants the lease. **The lock file must be on a volume every GPU container mounts** — per-container `/tmp` coordinates nothing.
31
+ - `gpu.py` — GPU tenancy (from argus-lens): the `GpuCoordinator` protocol (`lease(caller=, min_vram_mb=, priority=, target=, kind=, preemptible=, constraints=)` and async `alease()`, both yielding a `GpuLease` handle; local coordinators ignore the broker-only arguments), `NullCoordinator`, `LeaseCoordinator` (host-wide `flock` **then** a wait for NVML free VRAM, so tenants outside the lock like ComfyUI and Ollama are accounted for), `free_vram_mb()`, `coordinator_from_env()` (`ARGUS_GPU_*` — host-wide names on purpose: every process must agree). NVML lazily, behind `[gpu]`; a probe that cannot run grants the lease. **The lock file must be on a volume every GPU container mounts** — per-container `/tmp` coordinates nothing.
32
+ - `gpu_wire.py` — the GPU broker's wire models (pydantic only): `LeaseRequest`/`LeaseStatus`/`Heartbeat`/`HeartbeatReply`/`LeaseRelease` (`lease_version`) and `TenantManifest` (`tenant_version`, the `gpu-tenant.yaml` every GPU-using repo commits), with `lease_schema()`/`tenant_schema()` rendering the committed `gpu-lease-schema.json`/`gpu-tenant-schema.json`. **argus-iris serves this contract — change it only in step with iris**, and additively within a major (replies ignore unknown fields; manifests forbid them, so a new manifest field needs the schema regenerated and iris bumped).
33
+ - `gpu_broker.py` — `BrokerCoordinator`, the client for argus-iris (`ARGUS_GPU_COORDINATOR=broker`): register manifest → `POST /leases` → long-poll `GET /leases/{id}?wait_s=` → background heartbeat (sets `should_yield()`, re-attaches with `resume` on 404) → `POST /leases/{id}/release` with outcome and peak VRAM. **Fails open** to its fallback coordinator on transport errors, 5xx or a missing route; raises `BrokerError` on other 4xx or an incompatible major. httpx lazily, behind `[remote]`.
30
34
  - `xmp.py` — the suite's XMP sidecar read/merge/write (`write_xmp`/`read_xmp`/`merge_xmp`/`parse_xmp`, `XmpData`/`XmpRegions`, `XmpError`), used by lens (keywords/caption) and curator (faces, content rating). Stdlib `xml.etree` only. **Writes merge — latest wins per property, foreign properties preserved; keep that.** `None` = leave untouched, empty = remove. A DTD is refused and an unparseable sidecar raises rather than being overwritten. The `argus` namespace URI is a wire contract — don't change it.
31
- - `moderation/` — content-moderation clients over HTTP: `nudenet.py` (`NudeNetClient`, `NsfwResult`, `DEFAULT_EXPOSED_LABELS` — **the suite's one definition of nudity; don't widen it casually**, every stage's NSFW numbers move with it), `singguard.py` (`SingGuardClient` incl. text-only `check_text()`, `GuardResult`, `rule_matches`), `policies.py` (the named policy texts, the per-policy illegal-rule table and `mode()`: `default`/`sfw-*` are SFW — **a policy's text and its illegal letters change together**), `_http.py` (shared POST, proxy-free opener, `CheckFailed`/`ModerationConfigError`). Stdlib + pydantic; Pillow lazily, behind `[images]`. Never import NudeNet (AGPL) — HTTP only. URLs are passed in; stages own their env config.
35
+ - `moderation/` — content-moderation clients over HTTP: `nudenet.py` (`NudeNetClient`, `NsfwResult`, `DEFAULT_EXPOSED_LABELS` — **the suite's one definition of nudity; don't widen it casually**, every stage's NSFW numbers move with it), `singguard.py` (`SingGuardClient` incl. text-only `check_text()`, `GuardResult`, `rule_matches`), `policies.py` (the named policy texts, the per-policy illegal-rule table and `mode()`: `default`/`sfw-*` are SFW, the known adult policies NSFW, any other name raises — **a policy's text and its illegal letters change together**), `_http.py` (shared POST, proxy-free opener, `CheckFailed`/`ModerationConfigError`). Stdlib + pydantic; Pillow lazily, behind `[images]`. Never import NudeNet (AGPL) — HTTP only. URLs are passed in; stages own their env config.
32
36
  - `store/` — the optional stateful-services layer, opt-in per `CORTEX_*` env var: `lineage.py` (Phase 1 Postgres DAG, `open_lineage_store()`), `vector.py` (Phase 2 Qdrant, `open_vector_store()`), `blob.py` (Phase 3 MinIO/S3, `open_blob_store()`), over `config.py`/`models.py`/`errors.py`. Each `open_*()` returns a `Null*` no-op when its URL is unset. Drivers are behind `[postgres]`/`[qdrant]`/`[s3]`.
33
37
 
34
- There is **no CLI in this repo**. The one committed schema is `taxonomy-schema.json` (from `taxonomy_schema()`; `make schema` regenerates it and `tests/test_taxonomy.py` fails when it is stale). Studio generates its taxonomy types from it, so a `taxonomy.py` change needs `make schema` in the same commit.
38
+ There is **no CLI in this repo**. The committed schemas are `taxonomy-schema.json` (from `taxonomy_schema()`), `provenance-schema.json` (from `provenance_schema()`), `consent-schema.json` (from `consent_schema()`), and `gpu-lease-schema.json` / `gpu-tenant-schema.json` (from `gpu_wire`); `make schema` regenerates all five and `tests/test_taxonomy.py` / `tests/test_provenance.py` / `tests/test_consent.py` / `tests/test_gpu_wire.py` fail when one is stale. Studio generates its taxonomy types from the first, so a `taxonomy.py`, `provenance.py`, `consent.py` or `gpu_wire.py` change needs `make schema` in the same commit.
35
39
 
36
40
  ## Commands
37
41
 
@@ -55,7 +59,7 @@ Run one test: `uv run --no-sync pytest tests/test_wire.py::test_name -q`.
55
59
 
56
60
  ## Branches
57
61
 
58
- `dev` is the default branch and the base for every PR. Cut feature branches from `origin/dev` and bring them up to date by merging `origin/dev`, never `main`. Never open a PR against `main` or push work to it directly: `main` must never be ahead of `dev`. A release fast-forwards `main` to a `dev` commit that passed CI (`git push origin origin/dev:main`), then tags it. If `main` is protected and needs a PR, merge the release PR, then immediately merge `main` back into `dev` so the merge commit is not left on `main` alone.
62
+ `dev` is the default branch and the base for every PR. Cut feature branches from `origin/dev` and bring them up to date by merging `origin/dev`, never `main`. Never push work to `main` or open a PR against it, except the release PR from `dev`: `main` must never be ahead of `dev`, with one exception. A release fast-forwards `main` to a `dev` commit that passed CI (`git push origin origin/dev:main`), then tags it. If `main` is protected, the release is a reviewed PR from `dev` into `main`, and merging it leaves a merge commit on `main` alone. That merge commit is the one exception to the rule: it leaves `main` one commit ahead of `dev` with an identical tree. Leave it there and don't merge `main` back into `dev`. Anything else on `main` that `dev` lacks is a mistake (`git diff origin/dev origin/main` must be empty). Once `main` carries such a commit it can't be fast-forwarded, so later releases are PRs too.
59
63
 
60
64
  ## CI / release
61
65
 
@@ -20,8 +20,8 @@ format: ## Ruff autoformat + autofix
20
20
  test: ## Run the test suite
21
21
  $(UV) run --no-sync pytest --tb=short -q
22
22
 
23
- schema: ## Regenerate the committed taxonomy-schema.json
24
- $(UV) run --no-sync python -c "from pathlib import Path; from argus_cortex.taxonomy import taxonomy_schema; Path('taxonomy-schema.json').write_text(taxonomy_schema(), encoding='utf-8')"
23
+ schema: ## Regenerate the committed taxonomy, provenance, consent, gpu-lease and gpu-tenant schemas
24
+ $(UV) run --no-sync python -c "from pathlib import Path; from argus_cortex.taxonomy import taxonomy_schema; from argus_cortex.provenance import provenance_schema; from argus_cortex.consent import consent_schema; from argus_cortex.gpu_wire import lease_schema, tenant_schema; [Path(f).write_text(r(), encoding='utf-8') for f, r in (('taxonomy-schema.json', taxonomy_schema), ('provenance-schema.json', provenance_schema), ('consent-schema.json', consent_schema), ('gpu-lease-schema.json', lease_schema), ('gpu-tenant-schema.json', tenant_schema))]"
25
25
 
26
26
  build: ## Build sdist + wheel
27
27
  $(UV) build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argus-cortex
3
- Version: 0.4.7
3
+ Version: 0.6.0
4
4
  Summary: Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract
5
5
  Project-URL: Repository, https://github.com/smk762/argus-cortex
6
6
  Author: smk762
@@ -25,6 +25,7 @@ Requires-Dist: pillow>=10.0; extra == 'dev'
25
25
  Requires-Dist: psycopg[binary,pool]>=3.1; extra == 'dev'
26
26
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
27
  Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
28
29
  Requires-Dist: qdrant-client>=1.10; extra == 'dev'
29
30
  Requires-Dist: ruff==0.15.16; extra == 'dev'
30
31
  Requires-Dist: starlette>=0.37; extra == 'dev'
@@ -45,9 +46,16 @@ Requires-Dist: certifi>=2026.10.19; extra == 's3'
45
46
  Requires-Dist: minio>=7.2; extra == 's3'
46
47
  Provides-Extra: server
47
48
  Requires-Dist: starlette>=0.37; extra == 'server'
49
+ Provides-Extra: yaml
50
+ Requires-Dist: pyyaml>=6.0; extra == 'yaml'
48
51
  Description-Content-Type: text/markdown
49
52
 
50
- # argus-cortex
53
+ <h1>
54
+ <picture>
55
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smk762/argus-cortex/dev/brand/packages/cortex/argus-cortex-lockup-dark.svg">
56
+ <img alt="argus-cortex" src="https://raw.githubusercontent.com/smk762/argus-cortex/dev/brand/packages/cortex/argus-cortex-lockup-light.svg" height="52">
57
+ </picture>
58
+ </h1>
51
59
 
52
60
  Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
53
61
 
@@ -56,12 +64,16 @@ Part of the Argus suite; [SUITE.md](https://github.com/smk762/argus-cortex/blob/
56
64
  ## What's here
57
65
 
58
66
  - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim. `taxonomy_schema()` renders them into the committed `taxonomy-schema.json` (`make schema` regenerates it; a test fails when it is stale), so non-Python clients generate their types instead of copying the literals.
67
+ - **`argus_cortex.provenance`**: `Provenance` / `ProvenanceKind`, where a face, voice or image came from (the shape argus-mask#19 defined). `personal` needs only a `source` and is never `cleared`; `stock`, `synthetic` and `consented` are cleared, each states its licence, and `consented` also names its `subject` and the `release_sha256` of the signed release (never the document). `curated` is reserved and not cleared. Strict: unknown fields and a blank `source` are refused, `licence` is read as `license`, and a stored `cleared` is recomputed. A `synthetic` or `personal` source may name its `generator` (`model`, `model_sha256`, `seed`, `prompt`; optional, refused on other kinds). `provenance_schema()` renders the committed `provenance-schema.json` (`make schema`; a test fails when it is stale).
68
+ - **`argus_cortex.consent`**: a real person's likeness release, as argus-mask#26 defined it. `ConsentRecord` is keyed by the `release_sha256` a `consented` `Provenance` names, and holds the subject, who collected it, signing and expiry dates (expiry must be stated, `null` for none), the age check (who and when, never the ID), the `face`/`voice` opt-ins, voice `deliveries`, `uses` (`avatar`, `video`, `speech`, `training`; training is off unless listed), sha256s of the reference images and recordings, revocation terms and any `revocation`. `consent_valid(record, use=, likeness=, at=, delivery=)` returns a `ConsentVerdict` (`ok`, `reason`, `detail`), checking in order `revoked`, `not-yet-signed`, `expired`, `likeness-not-covered`, `use-not-covered`, `delivery-not-covered`; `at` must be timezone-aware and dates compare as UTC days, so the check never reads the clock. `consent_matches(record, provenance)` checks the record is the release and subject a provenance names. A record names a person, so services return only verdicts, never records. `consent_schema()` renders the committed `consent-schema.json`.
59
69
  - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
60
70
  - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
61
- - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
62
- - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib only.
71
+ - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard; a same-host write is trusted only for an IP literal, `localhost` or a name in `trusted_hosts`, so DNS rebinding cannot pass it), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
72
+ - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib + pydantic only.
63
73
  - **`argus_cortex.moderation`** — the suite's content-moderation clients, over HTTP to servers the deployer runs (nothing here imports a detector): `NudeNetClient` (a NudeNet server's `/infer` → `NsfwResult`: detections, per-class maxima, and an **exposed score**) and `SingGuardClient` (a SingGuard server's `/api/guard/check` → `GuardResult`: verdict, the policy rules that fired, explanation; optional custom policy text). `DEFAULT_EXPOSED_LABELS` is the suite's **one definition of nudity**: the five exposed buttocks/breast/genitalia/anus classes — faces, covered parts, and bare feet, belly, armpits or male chest never count. Stdlib + pydantic; Pillow (for SingGuard's downscaling of large images, on by default) is behind `[images]`: without it `SingGuardClient(...)` raises `ModerationConfigError` at construction unless given `downscale=False`. A `user:password@` in a URL is sent as HTTP Basic auth. URLs are passed in, never read from the environment here: each stage owns its config. NudeNet itself is AGPL-3.0, which is why it stays behind an HTTP boundary.
64
- - **`argus_cortex.gpu`**: GPU tenancy, generalised from argus-lens. `coordinator_from_env()` returns a coordinator whose `lease(caller=, min_vram_mb=)` holds a GPU slot for a block of work. `none` (the default) grants every lease at once. `lease` takes an exclusive `flock` that every argus process on the host shares (one lock for the host, not per GPU), then waits until `free_vram_mb()` (NVML, behind `[gpu]`) reports `min_vram_mb` free across *every* process on the card, ComfyUI and Ollama included, which never take the lock. It raises `GpuLeaseTimeout` after `ARGUS_GPU_LEASE_TIMEOUT_S` (300 s). The VRAM check is a snapshot, not a reservation, and a probe that cannot run lets the lease through. Containers each have their own `/tmp`, so set `ARGUS_GPU_LEASE_PATH` to a file on a volume every GPU service mounts, or the lock coordinates nothing.
74
+ - **`argus_cortex.gpu`**: GPU tenancy, generalised from argus-lens. `coordinator_from_env()` returns a coordinator whose `lease(caller=, min_vram_mb=)` holds a GPU slot for a block of work. `none` (the default) grants every lease at once. `lease` takes an exclusive `flock` that every argus process on the host shares (one lock for the host, not per GPU), then waits until `free_vram_mb()` (NVML, behind `[gpu]`) reports `min_vram_mb` free across *every* process on the card, ComfyUI and Ollama included, which never take the lock. It raises `GpuLeaseTimeout` after `ARGUS_GPU_LEASE_TIMEOUT_S` (300 s). The VRAM check is a snapshot, not a reservation, and a probe that cannot run lets the lease through. Containers each have their own `/tmp`, so set `ARGUS_GPU_LEASE_PATH` to a file on a volume every GPU service mounts, or the lock coordinates nothing. `ARGUS_GPU_DEVICE` (default `0`; an invalid value warns and falls back to `0`) picks which card NVML probes for free VRAM; the lock stays host-wide.
75
+ - **Broker.** `ARGUS_GPU_COORDINATOR=broker` asks the suite's GPU broker, argus-iris, at `ARGUS_GPU_BROKER_URL` (bearer `ARGUS_GPU_BROKER_TOKEN`, optional) instead. The broker places the lease on a node (morpheus, dragon, jarrah), queues it by priority, holds a real VRAM reservation, and evicts idle models to make room. `lease()` now also takes optional `priority` (`realtime`/`interactive`/`batch`/`background`), `target` (`comfyui`, `ollama:<model>`, `in-process`), `kind`, `preemptible`, `constraints` (`GpuConstraints`: `node`, `min_cc`, `bf16`) and `model_mb` (the part of `min_vram_mb` that is weights the service keeps loaded after the lease, such as a ComfyUI checkpoint or an Ollama model, so the broker counts them once rather than per lease); the local coordinators ignore them. Every coordinator yields a `GpuLease`: `node` and `base_url` say where the broker placed the work (call that URL; `None` means use your own), `should_yield()` turns true when the broker wants the GPU back (release at the next safe point; nothing is killed), and `report_peak_vram(mb)` feeds the broker's footprint learning. `alease()` is the async form on every coordinator; the file lease waits in a worker thread. Each lease carries a fresh `request_id`, so the one retry after a lost POST response returns the same lease rather than a second one. A broker lease heartbeats in the background, re-attaches if the broker restarts, and reports its outcome (`ok`/`failed`/`oom`/`cancelled`) on release. **It fails open**: unreachable or 5xx, and the lease comes from `ARGUS_GPU_BROKER_FALLBACK` (`lease`, the default, or `none`) with `via="fallback"`; a 4xx or an incompatible `lease_version` raises `BrokerError`, a refusal `GpuLeaseRefused`. httpx behind `[remote]`.
76
+ - **Tenant manifests.** Every repo that uses a GPU commits a `gpu-tenant.yaml` (`argus_cortex.gpu_wire.TenantManifest`): its kinds of work with target, declared VRAM, default priority and preemptibility, and the models it keeps resident with keepalive and idle-kill hints. `ARGUS_GPU_TENANT_MANIFEST` points the broker coordinator at it, and it is registered with the broker before the first lease; `ARGUS_GPU_TENANT` names the tenant if there is no manifest. `load_tenant_manifest()` reads YAML (behind `[yaml]`) or JSON. The lease wire models and the manifest render the committed `gpu-lease-schema.json` and `gpu-tenant-schema.json` (`make schema`; drift-tested), so non-Python callers and repos validate against those.
65
77
  - **`argus_cortex.store`** — the optional **stateful services layer** (the suite's persistent "memory"). External and opt-in — every store is configured by `CORTEX_*` env vars and no-ops when its URL is unset.
66
78
  - **Phase 1 — Postgres lineage store.** Persists the `source_asset → caption → human_edit → dataset_membership → training_run` DAG for LoRA reproducibility, edit capture, and the feedback loop. Backed by a psycopg connection **pool** (concurrent `asyncio.to_thread` calls are safe; a dropped connection is replaced), with a `transaction()` boundary for atomic multi-step writes. `open_lineage_store()` returns a no-op store when `CORTEX_PG_URL` is unset; psycopg lives behind the `[postgres]` extra.
67
79
  - **Phase 2 — Qdrant vector store.** Stores image + tag-set embeddings for retrieval-augmented few-shot and near-duplicate curation; put a `caption_id`/`asset_id` in a point's payload and a search hit joins straight back to the lineage. `open_vector_store()` no-ops when `CORTEX_QDRANT_URL` is unset; qdrant-client lives behind the `[qdrant]` extra. cortex stores vectors it's *handed* — computing embeddings is the caller's job.
@@ -105,7 +117,7 @@ result.exposed_score # 0.0-1.0 over DEFAULT_EXPOSED_LABELS; result.labels = p
105
117
  guard = SingGuardClient("http://guard:9150", policy=None) # None = SingGuard's default rules
106
118
  # thinking="fast" (default) is a verdict and rule; "fast-slow" also writes why; "slow" writes why
107
119
  # with no fast verdict ahead of the reasoning; "cascade" is fast, then slow only for what fast
108
- # flagged (the last two: argus-cull's singguard service only).
120
+ # flagged (the last two: agent-composer's guard or argus-cull's singguard service).
109
121
  verdict = guard.check(image_bytes, text="the prompt, if any")
110
122
  preflight = guard.check_text("the prompt") # no image: judge a prompt before it is run
111
123
  verdict.safe, verdict.rules # False, ["B. Real-World Crimes & Public Safety"]
@@ -121,7 +133,17 @@ illegal = [r for r in verdict.rules if policies.is_illegal("adult-granular", r)]
121
133
  policies.mode("adult-granular"), policies.mode("default") # "nsfw", "sfw"
122
134
  ```
123
135
 
124
- Stages ask for a policy by name; `policies` holds each one's text, the rules that mean illegal content under it, and its SFW/NSFW mode (`default` and `sfw-*` are SFW).
136
+ Stages ask for a policy by name; `policies` holds each one's text, the rules that mean illegal content under it, and its SFW/NSFW mode (`default` and `sfw-*` are SFW). The known names are `policies.POLICIES`; any other name raises `ModerationConfigError` (an `sfw-*` name gets mode `sfw` but has no text):
137
+
138
+ | Policy | Mode | Illegal rules |
139
+ |---|---|---|
140
+ | `default` (SingGuard's own rules, no text sent) | sfw | none |
141
+ | `adult` | nsfw | A |
142
+ | `adult-granular` | nsfw | A, H, I |
143
+ | `adult-cautious` | nsfw | A |
144
+ | `adult-cinematic` | nsfw | A |
145
+
146
+ `adult-granular`, `adult-cautious` and `adult-cinematic` are under evaluation against `adult`; one adult policy remains at release, beside `default`.
125
147
 
126
148
  ```python
127
149
  from argus_cortex.gpu import GpuLeaseTimeout, coordinator_from_env
@@ -134,6 +156,20 @@ except GpuLeaseTimeout:
134
156
  ... # no slot in time: back off, or answer 503
135
157
  ```
136
158
 
159
+ Through the broker, the same code can ask for a placement and honour a yield:
160
+
161
+ ```python
162
+ gpu = coordinator_from_env() # ARGUS_GPU_COORDINATOR=broker, ARGUS_GPU_BROKER_URL=http://morpheus:8120
163
+ async with gpu.alease(caller="expand", min_vram_mb=6000, target="ollama:qwen3:8b", priority="interactive") as lease:
164
+ ollama = lease.base_url or settings.ollama_url # dragon, or jarrah when dragon is full
165
+ ...
166
+ with gpu.lease(caller="bake", min_vram_mb=9000, target="comfyui", priority="batch", preemptible=True) as lease:
167
+ for tile in tiles:
168
+ if lease.should_yield():
169
+ break # requeue the rest; a higher-priority job wants the card
170
+ bake(tile)
171
+ ```
172
+
137
173
  Each `check()` raises `CheckFailed` (`NudeNetCheckFailed` / `GuardCheckFailed`) for a network error or an unusable answer, so a caller checking many images can record "unchecked" against the one image — and should treat unchecked as *not* safe. Bad configuration raises `ModerationConfigError`, whose message never echoes the URL.
138
174
 
139
175
  ```python
@@ -186,11 +222,14 @@ link = blobs.url(sha, expires=3600) # presigned GET URL
186
222
 
187
223
  ```bash
188
224
  uv pip install argus-cortex # core (pydantic only)
189
- uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend
225
+ uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend and the GPU broker client
190
226
  uv pip install "argus-cortex[server]" # + starlette for argus_cortex.server (WriteGuard, env_flag)
191
227
  uv pip install "argus-cortex[postgres]" # + psycopg for the lineage store
192
228
  uv pip install "argus-cortex[qdrant]" # + qdrant-client for the vector store
193
229
  uv pip install "argus-cortex[s3]" # + minio for the blob store
230
+ uv pip install "argus-cortex[images]" # + Pillow for SingGuardClient downscaling (on by default)
231
+ uv pip install "argus-cortex[gpu]" # + nvidia-ml-py for the GPU lease's free-VRAM probe
232
+ uv pip install "argus-cortex[yaml]" # + PyYAML for reading a gpu-tenant.yaml (JSON needs nothing)
194
233
  ```
195
234
 
196
235
  The stateful services are configured via `CORTEX_*` env vars — see [`.env.example`](.env.example). Each is optional: leave a URL unset and that feature degrades to a no-op.
@@ -1,4 +1,9 @@
1
- # argus-cortex
1
+ <h1>
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smk762/argus-cortex/dev/brand/packages/cortex/argus-cortex-lockup-dark.svg">
4
+ <img alt="argus-cortex" src="https://raw.githubusercontent.com/smk762/argus-cortex/dev/brand/packages/cortex/argus-cortex-lockup-light.svg" height="52">
5
+ </picture>
6
+ </h1>
2
7
 
3
8
  Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
4
9
 
@@ -7,12 +12,16 @@ Part of the Argus suite; [SUITE.md](https://github.com/smk762/argus-cortex/blob/
7
12
  ## What's here
8
13
 
9
14
  - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim. `taxonomy_schema()` renders them into the committed `taxonomy-schema.json` (`make schema` regenerates it; a test fails when it is stale), so non-Python clients generate their types instead of copying the literals.
15
+ - **`argus_cortex.provenance`**: `Provenance` / `ProvenanceKind`, where a face, voice or image came from (the shape argus-mask#19 defined). `personal` needs only a `source` and is never `cleared`; `stock`, `synthetic` and `consented` are cleared, each states its licence, and `consented` also names its `subject` and the `release_sha256` of the signed release (never the document). `curated` is reserved and not cleared. Strict: unknown fields and a blank `source` are refused, `licence` is read as `license`, and a stored `cleared` is recomputed. A `synthetic` or `personal` source may name its `generator` (`model`, `model_sha256`, `seed`, `prompt`; optional, refused on other kinds). `provenance_schema()` renders the committed `provenance-schema.json` (`make schema`; a test fails when it is stale).
16
+ - **`argus_cortex.consent`**: a real person's likeness release, as argus-mask#26 defined it. `ConsentRecord` is keyed by the `release_sha256` a `consented` `Provenance` names, and holds the subject, who collected it, signing and expiry dates (expiry must be stated, `null` for none), the age check (who and when, never the ID), the `face`/`voice` opt-ins, voice `deliveries`, `uses` (`avatar`, `video`, `speech`, `training`; training is off unless listed), sha256s of the reference images and recordings, revocation terms and any `revocation`. `consent_valid(record, use=, likeness=, at=, delivery=)` returns a `ConsentVerdict` (`ok`, `reason`, `detail`), checking in order `revoked`, `not-yet-signed`, `expired`, `likeness-not-covered`, `use-not-covered`, `delivery-not-covered`; `at` must be timezone-aware and dates compare as UTC days, so the check never reads the clock. `consent_matches(record, provenance)` checks the record is the release and subject a provenance names. A record names a person, so services return only verdicts, never records. `consent_schema()` renders the committed `consent-schema.json`.
10
17
  - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
11
18
  - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
12
- - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
13
- - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib only.
19
+ - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard; a same-host write is trusted only for an IP literal, `localhost` or a name in `trusted_hosts`, so DNS rebinding cannot pass it), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
20
+ - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib + pydantic only.
14
21
  - **`argus_cortex.moderation`** — the suite's content-moderation clients, over HTTP to servers the deployer runs (nothing here imports a detector): `NudeNetClient` (a NudeNet server's `/infer` → `NsfwResult`: detections, per-class maxima, and an **exposed score**) and `SingGuardClient` (a SingGuard server's `/api/guard/check` → `GuardResult`: verdict, the policy rules that fired, explanation; optional custom policy text). `DEFAULT_EXPOSED_LABELS` is the suite's **one definition of nudity**: the five exposed buttocks/breast/genitalia/anus classes — faces, covered parts, and bare feet, belly, armpits or male chest never count. Stdlib + pydantic; Pillow (for SingGuard's downscaling of large images, on by default) is behind `[images]`: without it `SingGuardClient(...)` raises `ModerationConfigError` at construction unless given `downscale=False`. A `user:password@` in a URL is sent as HTTP Basic auth. URLs are passed in, never read from the environment here: each stage owns its config. NudeNet itself is AGPL-3.0, which is why it stays behind an HTTP boundary.
15
- - **`argus_cortex.gpu`**: GPU tenancy, generalised from argus-lens. `coordinator_from_env()` returns a coordinator whose `lease(caller=, min_vram_mb=)` holds a GPU slot for a block of work. `none` (the default) grants every lease at once. `lease` takes an exclusive `flock` that every argus process on the host shares (one lock for the host, not per GPU), then waits until `free_vram_mb()` (NVML, behind `[gpu]`) reports `min_vram_mb` free across *every* process on the card, ComfyUI and Ollama included, which never take the lock. It raises `GpuLeaseTimeout` after `ARGUS_GPU_LEASE_TIMEOUT_S` (300 s). The VRAM check is a snapshot, not a reservation, and a probe that cannot run lets the lease through. Containers each have their own `/tmp`, so set `ARGUS_GPU_LEASE_PATH` to a file on a volume every GPU service mounts, or the lock coordinates nothing.
22
+ - **`argus_cortex.gpu`**: GPU tenancy, generalised from argus-lens. `coordinator_from_env()` returns a coordinator whose `lease(caller=, min_vram_mb=)` holds a GPU slot for a block of work. `none` (the default) grants every lease at once. `lease` takes an exclusive `flock` that every argus process on the host shares (one lock for the host, not per GPU), then waits until `free_vram_mb()` (NVML, behind `[gpu]`) reports `min_vram_mb` free across *every* process on the card, ComfyUI and Ollama included, which never take the lock. It raises `GpuLeaseTimeout` after `ARGUS_GPU_LEASE_TIMEOUT_S` (300 s). The VRAM check is a snapshot, not a reservation, and a probe that cannot run lets the lease through. Containers each have their own `/tmp`, so set `ARGUS_GPU_LEASE_PATH` to a file on a volume every GPU service mounts, or the lock coordinates nothing. `ARGUS_GPU_DEVICE` (default `0`; an invalid value warns and falls back to `0`) picks which card NVML probes for free VRAM; the lock stays host-wide.
23
+ - **Broker.** `ARGUS_GPU_COORDINATOR=broker` asks the suite's GPU broker, argus-iris, at `ARGUS_GPU_BROKER_URL` (bearer `ARGUS_GPU_BROKER_TOKEN`, optional) instead. The broker places the lease on a node (morpheus, dragon, jarrah), queues it by priority, holds a real VRAM reservation, and evicts idle models to make room. `lease()` now also takes optional `priority` (`realtime`/`interactive`/`batch`/`background`), `target` (`comfyui`, `ollama:<model>`, `in-process`), `kind`, `preemptible`, `constraints` (`GpuConstraints`: `node`, `min_cc`, `bf16`) and `model_mb` (the part of `min_vram_mb` that is weights the service keeps loaded after the lease, such as a ComfyUI checkpoint or an Ollama model, so the broker counts them once rather than per lease); the local coordinators ignore them. Every coordinator yields a `GpuLease`: `node` and `base_url` say where the broker placed the work (call that URL; `None` means use your own), `should_yield()` turns true when the broker wants the GPU back (release at the next safe point; nothing is killed), and `report_peak_vram(mb)` feeds the broker's footprint learning. `alease()` is the async form on every coordinator; the file lease waits in a worker thread. Each lease carries a fresh `request_id`, so the one retry after a lost POST response returns the same lease rather than a second one. A broker lease heartbeats in the background, re-attaches if the broker restarts, and reports its outcome (`ok`/`failed`/`oom`/`cancelled`) on release. **It fails open**: unreachable or 5xx, and the lease comes from `ARGUS_GPU_BROKER_FALLBACK` (`lease`, the default, or `none`) with `via="fallback"`; a 4xx or an incompatible `lease_version` raises `BrokerError`, a refusal `GpuLeaseRefused`. httpx behind `[remote]`.
24
+ - **Tenant manifests.** Every repo that uses a GPU commits a `gpu-tenant.yaml` (`argus_cortex.gpu_wire.TenantManifest`): its kinds of work with target, declared VRAM, default priority and preemptibility, and the models it keeps resident with keepalive and idle-kill hints. `ARGUS_GPU_TENANT_MANIFEST` points the broker coordinator at it, and it is registered with the broker before the first lease; `ARGUS_GPU_TENANT` names the tenant if there is no manifest. `load_tenant_manifest()` reads YAML (behind `[yaml]`) or JSON. The lease wire models and the manifest render the committed `gpu-lease-schema.json` and `gpu-tenant-schema.json` (`make schema`; drift-tested), so non-Python callers and repos validate against those.
16
25
  - **`argus_cortex.store`** — the optional **stateful services layer** (the suite's persistent "memory"). External and opt-in — every store is configured by `CORTEX_*` env vars and no-ops when its URL is unset.
17
26
  - **Phase 1 — Postgres lineage store.** Persists the `source_asset → caption → human_edit → dataset_membership → training_run` DAG for LoRA reproducibility, edit capture, and the feedback loop. Backed by a psycopg connection **pool** (concurrent `asyncio.to_thread` calls are safe; a dropped connection is replaced), with a `transaction()` boundary for atomic multi-step writes. `open_lineage_store()` returns a no-op store when `CORTEX_PG_URL` is unset; psycopg lives behind the `[postgres]` extra.
18
27
  - **Phase 2 — Qdrant vector store.** Stores image + tag-set embeddings for retrieval-augmented few-shot and near-duplicate curation; put a `caption_id`/`asset_id` in a point's payload and a search hit joins straight back to the lineage. `open_vector_store()` no-ops when `CORTEX_QDRANT_URL` is unset; qdrant-client lives behind the `[qdrant]` extra. cortex stores vectors it's *handed* — computing embeddings is the caller's job.
@@ -56,7 +65,7 @@ result.exposed_score # 0.0-1.0 over DEFAULT_EXPOSED_LABELS; result.labels = p
56
65
  guard = SingGuardClient("http://guard:9150", policy=None) # None = SingGuard's default rules
57
66
  # thinking="fast" (default) is a verdict and rule; "fast-slow" also writes why; "slow" writes why
58
67
  # with no fast verdict ahead of the reasoning; "cascade" is fast, then slow only for what fast
59
- # flagged (the last two: argus-cull's singguard service only).
68
+ # flagged (the last two: agent-composer's guard or argus-cull's singguard service).
60
69
  verdict = guard.check(image_bytes, text="the prompt, if any")
61
70
  preflight = guard.check_text("the prompt") # no image: judge a prompt before it is run
62
71
  verdict.safe, verdict.rules # False, ["B. Real-World Crimes & Public Safety"]
@@ -72,7 +81,17 @@ illegal = [r for r in verdict.rules if policies.is_illegal("adult-granular", r)]
72
81
  policies.mode("adult-granular"), policies.mode("default") # "nsfw", "sfw"
73
82
  ```
74
83
 
75
- Stages ask for a policy by name; `policies` holds each one's text, the rules that mean illegal content under it, and its SFW/NSFW mode (`default` and `sfw-*` are SFW).
84
+ Stages ask for a policy by name; `policies` holds each one's text, the rules that mean illegal content under it, and its SFW/NSFW mode (`default` and `sfw-*` are SFW). The known names are `policies.POLICIES`; any other name raises `ModerationConfigError` (an `sfw-*` name gets mode `sfw` but has no text):
85
+
86
+ | Policy | Mode | Illegal rules |
87
+ |---|---|---|
88
+ | `default` (SingGuard's own rules, no text sent) | sfw | none |
89
+ | `adult` | nsfw | A |
90
+ | `adult-granular` | nsfw | A, H, I |
91
+ | `adult-cautious` | nsfw | A |
92
+ | `adult-cinematic` | nsfw | A |
93
+
94
+ `adult-granular`, `adult-cautious` and `adult-cinematic` are under evaluation against `adult`; one adult policy remains at release, beside `default`.
76
95
 
77
96
  ```python
78
97
  from argus_cortex.gpu import GpuLeaseTimeout, coordinator_from_env
@@ -85,6 +104,20 @@ except GpuLeaseTimeout:
85
104
  ... # no slot in time: back off, or answer 503
86
105
  ```
87
106
 
107
+ Through the broker, the same code can ask for a placement and honour a yield:
108
+
109
+ ```python
110
+ gpu = coordinator_from_env() # ARGUS_GPU_COORDINATOR=broker, ARGUS_GPU_BROKER_URL=http://morpheus:8120
111
+ async with gpu.alease(caller="expand", min_vram_mb=6000, target="ollama:qwen3:8b", priority="interactive") as lease:
112
+ ollama = lease.base_url or settings.ollama_url # dragon, or jarrah when dragon is full
113
+ ...
114
+ with gpu.lease(caller="bake", min_vram_mb=9000, target="comfyui", priority="batch", preemptible=True) as lease:
115
+ for tile in tiles:
116
+ if lease.should_yield():
117
+ break # requeue the rest; a higher-priority job wants the card
118
+ bake(tile)
119
+ ```
120
+
88
121
  Each `check()` raises `CheckFailed` (`NudeNetCheckFailed` / `GuardCheckFailed`) for a network error or an unusable answer, so a caller checking many images can record "unchecked" against the one image — and should treat unchecked as *not* safe. Bad configuration raises `ModerationConfigError`, whose message never echoes the URL.
89
122
 
90
123
  ```python
@@ -137,11 +170,14 @@ link = blobs.url(sha, expires=3600) # presigned GET URL
137
170
 
138
171
  ```bash
139
172
  uv pip install argus-cortex # core (pydantic only)
140
- uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend
173
+ uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend and the GPU broker client
141
174
  uv pip install "argus-cortex[server]" # + starlette for argus_cortex.server (WriteGuard, env_flag)
142
175
  uv pip install "argus-cortex[postgres]" # + psycopg for the lineage store
143
176
  uv pip install "argus-cortex[qdrant]" # + qdrant-client for the vector store
144
177
  uv pip install "argus-cortex[s3]" # + minio for the blob store
178
+ uv pip install "argus-cortex[images]" # + Pillow for SingGuardClient downscaling (on by default)
179
+ uv pip install "argus-cortex[gpu]" # + nvidia-ml-py for the GPU lease's free-VRAM probe
180
+ uv pip install "argus-cortex[yaml]" # + PyYAML for reading a gpu-tenant.yaml (JSON needs nothing)
145
181
  ```
146
182
 
147
183
  The stateful services are configured via `CORTEX_*` env vars — see [`.env.example`](.env.example). Each is optional: leave a URL unset and that feature degrades to a no-op.