Metadata-Version: 2.4
Name: greenlandai
Version: 0.5.17
Summary: Unified SDK for the GreenlandAI ecosystem (ENYAL, JoulePAI, RAREEAI, GreenlandAI)
License: Apache-2.0
Project-URL: Homepage, https://greenlandai.ai
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24
Requires-Dist: cryptography>=41
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
Provides-Extra: bip39
Requires-Dist: mnemonic>=0.21; extra == "bip39"
Provides-Extra: pq
Requires-Dist: kyber-py==1.2.0; extra == "pq"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: mnemonic>=0.21; extra == "dev"
Requires-Dist: kyber-py==1.2.0; extra == "dev"
Dynamic: license-file

# greenlandai (Python)

Unified SDK for the GreenlandAI ecosystem — ENYAL, JoulePAI, RAREEAI, GreenlandAI.
Sits alongside the ENYAL-only SDK (`enyal-sdk` on PyPI / `@enyalai/sdk` on npm, 2.2.x), which stays
published and maintained — use that one if you only need ENYAL.

## Install
```bash
pip install greenlandai            # core
pip install "greenlandai[bip39]"   # + mnemonic recovery primitives
pip install "greenlandai[otel]"    # + OpenTelemetry metrics (opt-in; OFF by default)
```

## Auth
```python
from greenlandai import Client
client = Client(api_key="gai_...")          # GreenlandAI agent key — the graph (client.gldai) + the marketplace (client.rareeai)
client = Client(api_key="eyl_...")          # ENYAL API key — ENYAL calls (client.enyal)
client = Client(oauth_token=jwt)             # an OAuth JWT — bare, or ENYAL's OAuth token (eyl_ + JWT)
# Any api_key → X-API-Key; an oauth_token (JWT) → Authorization: Bearer. Pass exactly one.
# Passing an API key via oauth_token (or a JWT via api_key) raises immediately — no silent mis-route.
# 401s surface to you (no auto-refresh); hook: Client(..., on_auth_expired=cb)
# Key rotation = generate a new key server-side and rebuild the Client (no dual-validation window).
```

> **`gai_` + marketplace — resolved in 0.3.2:** `client.rareeai.*` now routes through the
> GreenlandAI door (`greenlandai.ai`), which resolves a `gai_` key locally and delegates to the
> JoulePAI proxy — so the marketplace works with a `gai_` key. (`<= 0.3.1` pointed at joulepai.ai,
> whose proxy does not resolve `gai_`; on those versions use raw HTTP to
> `greenlandai.ai/api/v1/marketplace/*`.) `eyl_` / OAuth marketplace access is unaffected.

## Hello world (one per submodule)
```python
# agent_id must be your own: an id another account has used is refused (403), and web-console / web-app /
# share-extension are reserved for the account owner's apps. Your wallet id (client.enyal.get_account()["wallet_id"])
# is always yours. Messages are addressed by the recipient's JoulePAI wallet id.
client.enyal.archive("<your-own-agent-id>", "decision_record", "k1", {"decision": "x"})
client.joulepai.transfer("@bob", 1000, note="thanks")     # auto idempotency key
client.rareeai.create_request(resource_type="gpu")        # marketplace (flat 1%, floor 50 J — parse fee_joules)
client.rareeai.oracle_assignments()                       # oracle queue (0.3.6; the pool is invite-only at launch)
client.gldai.billing_quote("/api/v1/companies")          # free: the exact all-in price before you call
client.gldai.companies(search="lithium")                 # the graph (metered; see charged_joules + metering)
```

## The graph (client.gldai)
The physical economy as queryable data: companies, resource deposits, infrastructure assets, projects and
commodities, and the relationships between them (ownership, operation, supply). Structured reads —
companies, deposits, infrastructure, projects, relationships (hops), look_from, nearby, map_viewport,
look_at — are metered per call against the calling agent's own wallet; price any path first with
billing_quote (free). Live totals: `GET https://greenlandai.ai/api/v1/public/stats` (public).
`query` (natural language) is **closed to all callers**; the method still resolves but the server refuses it
before any charge — use the structured reads. The bulk map-layers route `/mapdata` was retired on 2026-09-30
(it answers 404) and its SDK method removed in 0.5.13 — use map_viewport for map reads.

## Encrypted backup of the local knowledge graph (greenlandai.enyal.agent.EnyalAgent)
```python
from greenlandai.enyal.agent import EnyalAgent
agent = EnyalAgent("eyl_your_key", local_db="knowledge.db")
agent.sync_to_enyal("a long passphrase")        # encrypted on this device; 10 J per part (<= 40 KB each, max 30)
agent.restore_from_enyal("a long passphrase")   # 100 J per part; zero-knowledge accounts: customer_share="<66 hex>"
```
Same format as enyal-sdk and @enyalai/sdk (a backup from one restores in the others).
- **Your password is the only protection — choose a long, unique one.** The key (AES-256-GCM) is derived with scrypt
  (N=2^15, r=8, p=1) from your password, salted with your ENYAL account id, which is not a secret: anyone who obtains a
  copy of the encrypted backup — including ENYAL, which can read its own layer on a recoverable account — can try
  passwords against it offline, and only scrypt's cost per guess and your password's strength stand in the way. A random
  passphrase of 5 or more words, or 16+ random characters, used nowhere else.
- **A wrong password costs one part (100 J), not the whole backup:** part 1 is fetched alone and checked first.
- **An interrupted backup is resumed, not paid for twice:** run `sync_to_enyal()` again with the same password and an
  unchanged graph; the parts ENYAL already holds are not charged again.

## ENYAL: zero-knowledge records, proof refusals and binding notes (0.5.13)

- **Zero-knowledge records.** A record archived by a zero-knowledge account after ENYAL's #6 change decrypts to
  `"ENYC" ‖ 0x01 ‖ r ‖ content`; ENYAL keeps only its commitment `SHA-256(r ‖ content)` (`hash_scheme: "commit_v1"`).
  `client.enyal.crypto.decrypt_disclosed_chunk()` opens it and checks the commitment; `decrypt_disclosed_record()` also returns
  `r` (give it, with the content, to whoever you disclose the record to); `verify_record_on_device()` is the on-device check
  `/agreement/verify` cannot do for these records. A record that is not what was archived raises `RecordIntegrityError`
  (`.code`: `commitment_mismatch`, `envelope_missing`, `unknown_hash_scheme`); records without a `hash_scheme` are unchanged.
- **Proof refusals.** ENYAL's uncharged 409 refusals (proofs over zero-knowledge records, `/agreement/verify` on one) raise
  `greenlandai.ProofRefusedError` (a ConflictError; `.joule_cost` 0), not retried. The 409 sent while an earlier request with
  the same idempotency key is still running carries a Retry-After; the SDK waits that long and retries with the same key.
- **Free replies and binding notes.** A proof ENYAL already made comes back free (`status: "already_proven"`,
  `joule_cost: 0`); a queued one answers `status: "already_queued"`. Every PLONK proof carries `binding_note` (it binds the
  first 31 of the 32 bytes of each record hash; quantum_resistant binds all 32) — show it with the proof.

## Idempotency promise matrix (auto-retry behavior — design §6, post-G1 2026-06-10)

| Method (py / js) | Wire class | Tier | Auto-retry |
|---|---|---|---|
| enyal.archive | A body client_chunk_id | ENFORCED | ON |
| enyal.timestamp / create_agreement | A body client_chunk_id | ENFORCED | ON |
| enyal.compliance_attest | A body client_attestation_id | ENFORCED | ON |
| enyal.prove / prove_batch / prove_share_combination | B body idempotency_key | ENFORCED | ON |
| enyal.disclose / request_client_disclosure | B | ENFORCED | ON |
| enyal.send_message | B | ENFORCED | ON |
| enyal.knowledge_upgrade · memory_structured_query · memory_content_query · queue_proof | — | NOT SUPPORTED | **OFF** (retries may duplicate billed effects) |
| joulepai.transfer (pay) | D body idempotency_key (proc-enforced) | ENFORCED | ON |
| joulepai.fund | — | NOT SUPPORTED | **OFF** (duplicate Stripe sessions possible) |
| rareeai.create_provider/create_request/deliver/resolve | B | ENFORCED | ON |
| rareeai.approve/reject/dispute/extend_dispute | — | NOT SUPPORTED | **OFF** |
| gldai metered reads (companies/deposits/infrastructure/projects/relationships/look_from/nearby/map_viewport/look_at) | C header X-Idempotency-Key | ENFORCED | ON |
| all GET reads | — | safe | ON |

Every billed call auto-generates a UUID4 key (stable across retries) unless you pass `idempotency_key` / `idempotencyKey` yourself.

## Observability (OFF by default)
`Client(logger=my_logger, on_retry=..., on_terminal_failure=..., metrics=otel_metrics())` —
DEBUG logs redact Authorization + X-API-Key structurally.

Migration from enyal-sdk 2.1.0: see MIGRATION.md. Changelog: CHANGELOG.md.
