aweb-naapp 0.1.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.
- aweb_naapp-0.1.0/.gitignore +16 -0
- aweb_naapp-0.1.0/PKG-INFO +279 -0
- aweb_naapp-0.1.0/README.md +228 -0
- aweb_naapp-0.1.0/pyproject.toml +135 -0
- aweb_naapp-0.1.0/renderer/.gitignore +2 -0
- aweb_naapp-0.1.0/renderer/actions.mjs +704 -0
- aweb_naapp-0.1.0/renderer/build.mjs +30 -0
- aweb_naapp-0.1.0/renderer/catalogs.mjs +25 -0
- aweb_naapp-0.1.0/renderer/collections.mjs +481 -0
- aweb_naapp-0.1.0/renderer/dom.mjs +67 -0
- aweb_naapp-0.1.0/renderer/index.mjs +424 -0
- aweb_naapp-0.1.0/renderer/input.mjs +158 -0
- aweb_naapp-0.1.0/renderer/link-host.mjs +81 -0
- aweb_naapp-0.1.0/renderer/media.mjs +125 -0
- aweb_naapp-0.1.0/renderer/package-lock.json +704 -0
- aweb_naapp-0.1.0/renderer/package.json +20 -0
- aweb_naapp-0.1.0/renderer/playwright.config.mjs +13 -0
- aweb_naapp-0.1.0/renderer/sanitize.mjs +97 -0
- aweb_naapp-0.1.0/renderer/styles.css +665 -0
- aweb_naapp-0.1.0/renderer/tests/action-continuations.spec.mjs +188 -0
- aweb_naapp-0.1.0/renderer/tests/actions.spec.mjs +334 -0
- aweb_naapp-0.1.0/renderer/tests/collections.spec.mjs +148 -0
- aweb_naapp-0.1.0/renderer/tests/input.test.mjs +38 -0
- aweb_naapp-0.1.0/renderer/tests/links.test.mjs +31 -0
- aweb_naapp-0.1.0/renderer/tests/numbers.test.mjs +40 -0
- aweb_naapp-0.1.0/renderer/tests/render.spec.mjs +139 -0
- aweb_naapp-0.1.0/renderer/tests/schema.test.mjs +29 -0
- aweb_naapp-0.1.0/renderer/tests/update.spec.mjs +225 -0
- aweb_naapp-0.1.0/src/aweb_naapp/__init__.py +56 -0
- aweb_naapp-0.1.0/src/aweb_naapp/assets/aweb.css +849 -0
- aweb_naapp-0.1.0/src/aweb_naapp/assets/naapp-components.css +86 -0
- aweb_naapp-0.1.0/src/aweb_naapp/chrome.py +313 -0
- aweb_naapp-0.1.0/src/aweb_naapp/css.py +61 -0
- aweb_naapp-0.1.0/src/aweb_naapp/dispatch/__init__.py +401 -0
- aweb_naapp-0.1.0/src/aweb_naapp/dispatch/migrations/001_dispatches.sql +31 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/__init__.py +61 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/addresses.py +182 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/headers.py +154 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/refused.py +19 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/send.py +359 -0
- aweb_naapp-0.1.0/src/aweb_naapp/egress/urls.py +500 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/__init__.py +5 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/catalog.py +579 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/delivery.py +278 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/envelope.py +341 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/migrations/001_owner_act_nonces.sql +19 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/owner_act.py +529 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/owner_act_nonces.py +203 -0
- aweb_naapp-0.1.0/src/aweb_naapp/hooks/strictjson.py +207 -0
- aweb_naapp-0.1.0/src/aweb_naapp/idempotency/__init__.py +123 -0
- aweb_naapp-0.1.0/src/aweb_naapp/idempotency/migrations/001_idempotency_keys.sql +16 -0
- aweb_naapp-0.1.0/src/aweb_naapp/llms.py +74 -0
- aweb_naapp-0.1.0/src/aweb_naapp/manifest.py +101 -0
- aweb_naapp-0.1.0/src/aweb_naapp/ops/__init__.py +1 -0
- aweb_naapp-0.1.0/src/aweb_naapp/ops/__main__.py +100 -0
- aweb_naapp-0.1.0/src/aweb_naapp/ops/render.py +487 -0
- aweb_naapp-0.1.0/src/aweb_naapp/outbox/__init__.py +701 -0
- aweb_naapp-0.1.0/src/aweb_naapp/outbox/migrations/001_outbox.sql +59 -0
- aweb_naapp-0.1.0/src/aweb_naapp/outbox/migrations/002_outbox_channels.sql +13 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/__init__.py +18 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/ledger_migrations/001_deletions.sql +10 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/ledger_migrations/002_request_claims.sql +8 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/lifecycle.py +459 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/migrations/001_pages.sql +52 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/migrations/002_import_history.sql +5 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/migrations/003_operator_policy.sql +3 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/migrations/004_layout_context.sql +8 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/migrations/005_restore_generation.sql +6 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/models.py +141 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/store.py +995 -0
- aweb_naapp-0.1.0/src/aweb_naapp/page_store/support.py +50 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/__init__.py +36 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/assets/gallery-entry.js +100 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/assets/gallery-video.mp4 +0 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/assets/page-renderer.css +764 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/assets/page-renderer.js +9193 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/errors.py +34 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/gallery.py +396 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/gallery_host.py +349 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/lint.py +561 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/page-format-v1.schema.json +1843 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/render.py +82 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/text.py +132 -0
- aweb_naapp-0.1.0/src/aweb_naapp/pages/validation.py +219 -0
- aweb_naapp-0.1.0/src/aweb_naapp/reference.py +336 -0
- aweb_naapp-0.1.0/src/aweb_naapp/robots.py +12 -0
- aweb_naapp-0.1.0/src/aweb_naapp/roles/__init__.py +203 -0
- aweb_naapp-0.1.0/src/aweb_naapp/roles/migrations/001_role_assignments.sql +18 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/__init__.py +1 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/app.py +436 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/app_emit.py +109 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/app_keys.py +95 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/__init__.py +76 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/challenge.py +32 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/migrations/001_approvals.sql +67 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/model.py +320 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/request.py +87 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/service.py +750 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/approvals/store.py +440 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/attestation.py +258 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/audit.py +49 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/authorization.py +219 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/build_identity.py +112 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/db.py +48 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/event_emit.py +220 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/freshness.py +94 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/health.py +120 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/links.py +34 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/migrations/001_observed_principals.sql +20 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/origins.py +121 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/passkeys/__init__.py +53 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/passkeys/migrations/001_passkeys.sql +96 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/passkeys/rp.py +33 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/passkeys/service.py +1151 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/passkeys/webauthn.py +90 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/settings.py +77 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/team_auth.py +616 -0
- aweb_naapp-0.1.0/src/aweb_naapp/runtime/team_server.py +170 -0
- aweb_naapp-0.1.0/src/aweb_naapp/signing_key.py +82 -0
- aweb_naapp-0.1.0/src/aweb_naapp/skills.py +99 -0
- aweb_naapp-0.1.0/testing/pyproject.toml +25 -0
- aweb_naapp-0.1.0/testing/src/aweb_naapp_testing/__init__.py +215 -0
- aweb_naapp-0.1.0/testing/src/aweb_naapp_testing/network.py +271 -0
- aweb_naapp-0.1.0/testing/src/aweb_naapp_testing/owner_acts.py +102 -0
- aweb_naapp-0.1.0/testing/src/aweb_naapp_testing/webauthn.py +166 -0
- aweb_naapp-0.1.0/tests/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/conftest.py +3 -0
- aweb_naapp-0.1.0/tests/dispatch/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/dispatch/test_dispatch.py +606 -0
- aweb_naapp-0.1.0/tests/dispatch/test_dispatch_is_dependency_light.py +31 -0
- aweb_naapp-0.1.0/tests/egress/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/egress/test_addresses.py +91 -0
- aweb_naapp-0.1.0/tests/egress/test_egress_is_dependency_light.py +34 -0
- aweb_naapp-0.1.0/tests/egress/test_send.py +562 -0
- aweb_naapp-0.1.0/tests/egress/test_url_vectors.py +222 -0
- aweb_naapp-0.1.0/tests/hooks/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/hooks/conftest.py +3 -0
- aweb_naapp-0.1.0/tests/hooks/test_binding_cases.py +159 -0
- aweb_naapp-0.1.0/tests/hooks/test_catalog.py +211 -0
- aweb_naapp-0.1.0/tests/hooks/test_delivery.py +135 -0
- aweb_naapp-0.1.0/tests/hooks/test_envelope.py +87 -0
- aweb_naapp-0.1.0/tests/hooks/test_envelope_vector_payloads.py +50 -0
- aweb_naapp-0.1.0/tests/hooks/test_hooks_is_dependency_free.py +58 -0
- aweb_naapp-0.1.0/tests/hooks/test_owner_act_signing.py +123 -0
- aweb_naapp-0.1.0/tests/hooks/test_owner_act_vector.py +127 -0
- aweb_naapp-0.1.0/tests/hooks/test_owner_act_verifier.py +295 -0
- aweb_naapp-0.1.0/tests/idempotency/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/idempotency/test_idempotency.py +183 -0
- aweb_naapp-0.1.0/tests/idempotency/test_idempotency_is_dependency_light.py +31 -0
- aweb_naapp-0.1.0/tests/outbox/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/outbox/drain_child.py +41 -0
- aweb_naapp-0.1.0/tests/outbox/test_outbox.py +729 -0
- aweb_naapp-0.1.0/tests/outbox/test_outbox_is_dependency_light.py +31 -0
- aweb_naapp-0.1.0/tests/page_store/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/page_store/conftest.py +2 -0
- aweb_naapp-0.1.0/tests/page_store/test_layout_state.py +89 -0
- aweb_naapp-0.1.0/tests/page_store/test_lifecycle.py +400 -0
- aweb_naapp-0.1.0/tests/page_store/test_signed_runtime.py +179 -0
- aweb_naapp-0.1.0/tests/page_store/test_store.py +465 -0
- aweb_naapp-0.1.0/tests/pages/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/pages/test_page_format_vectors.py +203 -0
- aweb_naapp-0.1.0/tests/pages/test_page_proposal_vectors.py +40 -0
- aweb_naapp-0.1.0/tests/pages/test_render.py +33 -0
- aweb_naapp-0.1.0/tests/roles/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/roles/test_roles.py +298 -0
- aweb_naapp-0.1.0/tests/roles/test_roles_is_dependency_light.py +32 -0
- aweb_naapp-0.1.0/tests/runtime/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/__init__.py +0 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/conftest.py +13 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/support.py +273 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/test_approval_request.py +111 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/test_approvals.py +1000 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/test_links.py +29 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/test_passkeys.py +1206 -0
- aweb_naapp-0.1.0/tests/runtime/approvals/test_webauthn.py +192 -0
- aweb_naapp-0.1.0/tests/runtime/conftest.py +70 -0
- aweb_naapp-0.1.0/tests/runtime/test_authorization.py +352 -0
- aweb_naapp-0.1.0/tests/runtime/test_awid_team_cache.py +334 -0
- aweb_naapp-0.1.0/tests/runtime/test_did_aw_current_key.py +242 -0
- aweb_naapp-0.1.0/tests/runtime/test_freshness.py +283 -0
- aweb_naapp-0.1.0/tests/runtime/test_hostile_team_auth_headers.py +75 -0
- aweb_naapp-0.1.0/tests/runtime/test_naapp_runtime.py +451 -0
- aweb_naapp-0.1.0/tests/runtime/test_observed_principals.py +63 -0
- aweb_naapp-0.1.0/tests/runtime/test_origins.py +186 -0
- aweb_naapp-0.1.0/tests/runtime/test_principal_attestation.py +251 -0
- aweb_naapp-0.1.0/tests/runtime/test_runtime_health.py +93 -0
- aweb_naapp-0.1.0/tests/runtime/test_team_auth_envelope.py +259 -0
- aweb_naapp-0.1.0/tests/runtime/test_team_auth_vectors.py +85 -0
- aweb_naapp-0.1.0/tests/runtime/test_team_server_directory.py +189 -0
- aweb_naapp-0.1.0/tests/test_app_emit_credential.py +92 -0
- aweb_naapp-0.1.0/tests/test_app_keys.py +187 -0
- aweb_naapp-0.1.0/tests/test_db.py +148 -0
- aweb_naapp-0.1.0/tests/test_docs_surface_is_dependency_free.py +33 -0
- aweb_naapp-0.1.0/tests/test_event_emit.py +444 -0
- aweb_naapp-0.1.0/tests/test_health.py +279 -0
- aweb_naapp-0.1.0/tests/test_manifest_file.py +65 -0
- aweb_naapp-0.1.0/tests/test_naapp.py +360 -0
- aweb_naapp-0.1.0/tests/test_ops_render.py +458 -0
- aweb_naapp-0.1.0/tests/test_routes.py +274 -0
- aweb_naapp-0.1.0/tests/test_settings.py +120 -0
- aweb_naapp-0.1.0/tests/test_signing_key.py +151 -0
- aweb_naapp-0.1.0/tests/test_skills.py +98 -0
- aweb_naapp-0.1.0/uv.lock +1797 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aweb-naapp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable Native Agentic App (naapp) docs surface: shared chrome, the aweb design system, and manifest-driven llms.txt and reference generators.
|
|
5
|
+
Project-URL: Homepage, https://github.com/awebai/aweb/tree/main/naapp-lib
|
|
6
|
+
Project-URL: Repository, https://github.com/awebai/aweb
|
|
7
|
+
Project-URL: Issues, https://github.com/awebai/aweb/issues
|
|
8
|
+
Author-email: "aweb.ai" <hello@aweb.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,aweb,awid,manifest,naapp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Provides-Extra: dispatch
|
|
19
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'dispatch'
|
|
20
|
+
Provides-Extra: egress
|
|
21
|
+
Requires-Dist: dnspython>=2.7.0; extra == 'egress'
|
|
22
|
+
Requires-Dist: httpcore[asyncio]>=1.0.9; extra == 'egress'
|
|
23
|
+
Requires-Dist: idna>=3.10; extra == 'egress'
|
|
24
|
+
Requires-Dist: publicsuffixlist==1.1.0.20261018; extra == 'egress'
|
|
25
|
+
Provides-Extra: hooks
|
|
26
|
+
Requires-Dist: awid-service>=0.5.30; extra == 'hooks'
|
|
27
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'hooks'
|
|
28
|
+
Requires-Dist: pynacl>=1.6.2; extra == 'hooks'
|
|
29
|
+
Provides-Extra: idempotency
|
|
30
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'idempotency'
|
|
31
|
+
Provides-Extra: outbox
|
|
32
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'outbox'
|
|
33
|
+
Provides-Extra: page-store
|
|
34
|
+
Requires-Dist: jsonschema>=4.23; extra == 'page-store'
|
|
35
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'page-store'
|
|
36
|
+
Provides-Extra: pages
|
|
37
|
+
Requires-Dist: jsonschema>=4.23; extra == 'pages'
|
|
38
|
+
Provides-Extra: roles
|
|
39
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'roles'
|
|
40
|
+
Provides-Extra: runtime
|
|
41
|
+
Requires-Dist: awid-service>=0.5.30; extra == 'runtime'
|
|
42
|
+
Requires-Dist: fastapi>=0.116.1; extra == 'runtime'
|
|
43
|
+
Requires-Dist: httpx>=0.28.1; extra == 'runtime'
|
|
44
|
+
Requires-Dist: pgdbm>=0.5.0; extra == 'runtime'
|
|
45
|
+
Requires-Dist: pydantic-settings>=2.10.1; extra == 'runtime'
|
|
46
|
+
Requires-Dist: pynacl>=1.6.2; extra == 'runtime'
|
|
47
|
+
Requires-Dist: webauthn>=3.0.1; extra == 'runtime'
|
|
48
|
+
Provides-Extra: signing-key
|
|
49
|
+
Requires-Dist: pynacl>=1.6.2; extra == 'signing-key'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# aweb-naapp
|
|
53
|
+
|
|
54
|
+
The reusable Native Agentic App (naapp) docs surface. Every naapp built with it
|
|
55
|
+
serves the same standard surface — so the standard is shared code, not a doc
|
|
56
|
+
copied by hand.
|
|
57
|
+
|
|
58
|
+
An app supplies its canonical `aweb-app.json` manifest, a `SiteConfig` (brand,
|
|
59
|
+
nav, footer), and its own landing body, and gets:
|
|
60
|
+
|
|
61
|
+
- the **aweb design system** (Paper/Clay), vendored and sha-pinned, served at
|
|
62
|
+
`/css/aweb.css`;
|
|
63
|
+
- the **shared chrome** — head, header, footer, theme toggle, and copy button;
|
|
64
|
+
- **manifest-driven llms.txt blocks** — the operations list and the
|
|
65
|
+
team-certificate authentication section;
|
|
66
|
+
- the **`/reference` page** — every operation in parallel as the canonical
|
|
67
|
+
`aw <verb>` command and the raw HTTP wire format, with the honest team-cert v2
|
|
68
|
+
envelope linked to the conformance vector.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import aweb_naapp as naapp
|
|
72
|
+
|
|
73
|
+
site = naapp.SiteConfig(
|
|
74
|
+
origin="https://library.aweb.ai",
|
|
75
|
+
brand="library",
|
|
76
|
+
title="library — agent profiles for AWID teams",
|
|
77
|
+
description="...",
|
|
78
|
+
nav_links=(naapp.NavLink("Reference", "/reference"), ...),
|
|
79
|
+
footer_blurb="...",
|
|
80
|
+
footer_columns=(...),
|
|
81
|
+
footer_bottom="library is a Native Agentic App on the aweb.ai hub. AWID is the identity authority.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
reference_html = naapp.render_reference(manifest, site, verb="library")
|
|
85
|
+
landing_html = naapp.page(site, body_html, include_copy_llms=True)
|
|
86
|
+
css_bytes = naapp.aweb_css()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The docs surface is pure standard library. Request-time runtime code (the
|
|
90
|
+
team-auth verifier, approvals, passkeys, and team-server discovery from the
|
|
91
|
+
team's AWID `aweb.team-server/1` record) lives under `aweb_naapp.runtime` and
|
|
92
|
+
needs the `runtime` extra: `aweb-naapp[runtime]`. Two parts the core server
|
|
93
|
+
shares import without it: the durable outbox, `aweb_naapp.outbox`
|
|
94
|
+
(`aweb-naapp[outbox]`), and the signing-key reader, `aweb_naapp.signing_key`
|
|
95
|
+
(`aweb-naapp[signing-key]`).
|
|
96
|
+
|
|
97
|
+
The egress guard, `aweb_naapp.egress`, is for naapps that call an external
|
|
98
|
+
service with their own credential. It needs only the `egress` extra,
|
|
99
|
+
`aweb-naapp[egress]`, and none of the runtime. It canonicalizes and matches
|
|
100
|
+
outbound URLs under the contracts in `docs/vectors/egress-url-v2.json` and
|
|
101
|
+
`docs/vectors/egress-address-v1.json`, resolves names itself and refuses any
|
|
102
|
+
denied address, and sends a request only to the destination it matches,
|
|
103
|
+
through one pinned, verified connection. Redirects come back as responses, no proxy is read from
|
|
104
|
+
the environment, and a failure is classed as not sent or of unknown outcome.
|
|
105
|
+
|
|
106
|
+
### App keys
|
|
107
|
+
|
|
108
|
+
An app names its signing keys by kid, as key files:
|
|
109
|
+
`<PREFIX>APP_KEY_FILES='{"<kid>": "/run/secrets/<file>"}'`. A key file holds the
|
|
110
|
+
32-byte Ed25519 seed as 64 hex characters, with one trailing newline allowed. It
|
|
111
|
+
must be a regular file of at most 1 KiB, and must not be group- or world-writable. The manifest
|
|
112
|
+
declares which kid signs which purpose, and the app does not start unless the
|
|
113
|
+
configured key is the declared one. An app with `<PREFIX>APP_EVENTS_ORIGIN` set
|
|
114
|
+
(https, or http on loopback) must hold its declared app-event key. That key may instead be given as
|
|
115
|
+
`<PREFIX>APP_EMIT_KEY_SEED_HEX` with `<PREFIX>APP_EMIT_KID`, a form that signs
|
|
116
|
+
app events only.
|
|
117
|
+
|
|
118
|
+
The egress guard, `aweb_naapp.egress`, is for naapps that call an external
|
|
119
|
+
service with their own credential. It needs only the `egress` extra,
|
|
120
|
+
`aweb-naapp[egress]`, and none of the runtime. It canonicalizes and matches
|
|
121
|
+
outbound URLs under the contracts in `docs/vectors/egress-url-v2.json` and
|
|
122
|
+
`docs/vectors/egress-address-v1.json`, resolves names itself and refuses any
|
|
123
|
+
denied address, and sends a request only to the destination it matches,
|
|
124
|
+
through one pinned, verified connection. Redirects come back as responses, no proxy is read from
|
|
125
|
+
the environment, and a failure is classed as not sent or of unknown outcome.
|
|
126
|
+
|
|
127
|
+
### Health
|
|
128
|
+
|
|
129
|
+
`/live` reports the app's id and build commit. `/health` and `/ready` also run
|
|
130
|
+
the app's dependency checks and report each by name: `ready` is 503 when any
|
|
131
|
+
check fails, and `health` always answers 200. The runtime checks its database
|
|
132
|
+
and reports the age of its oldest undelivered outbox event
|
|
133
|
+
(`NaappRuntime.health_checks()`). An app adds its own as async callables that
|
|
134
|
+
return a `CheckResult`. A check that raises, or does not answer within 5 s,
|
|
135
|
+
fails, and its error is logged, never served. `/ready` serves only whether each
|
|
136
|
+
check passed; `/health` adds each check's detail, whose values are numbers,
|
|
137
|
+
booleans or null, never text. The routes are unauthenticated,
|
|
138
|
+
so the checks run at most once every 10 s: concurrent requests share the run in
|
|
139
|
+
flight, and later ones get its result until it expires.
|
|
140
|
+
|
|
141
|
+
### Agent and human origins
|
|
142
|
+
|
|
143
|
+
An app with pages for people in a browser serves them on their own origin.
|
|
144
|
+
`aweb_naapp.runtime.origins.split_origins(agent_app, human_app, agent_origin=...,
|
|
145
|
+
human_origin=...)` is one ASGI app that dispatches on `Host`:
|
|
146
|
+
|
|
147
|
+
- the agent origin serves the runtime's app, with no cookies in either
|
|
148
|
+
direction, so its only authority is the team-auth signature;
|
|
149
|
+
- the human origin refuses any request carrying `Authorization`, `X-AWEB-*` or
|
|
150
|
+
`X-AWID-*` (`403 team_auth_refused`);
|
|
151
|
+
- any other host is `421 unknown_origin`.
|
|
152
|
+
|
|
153
|
+
A route exists on one origin because it is declared in one of the two apps. The
|
|
154
|
+
lifespan is the agent app's.
|
|
155
|
+
|
|
156
|
+
## Approvals and passkeys
|
|
157
|
+
|
|
158
|
+
`aweb_naapp.runtime.approvals` is the single-use human approval primitive that
|
|
159
|
+
apps share. An agent proposes an exact request; the owner-designated human
|
|
160
|
+
approves or rejects it; a trusted dispatcher then admits exactly one dispatch of
|
|
161
|
+
that exact request.
|
|
162
|
+
|
|
163
|
+
- **Frozen request.** `request_sha256` is SHA-256 over
|
|
164
|
+
`aweb.approval-request/1\n` plus the canonical JSON of the request and its
|
|
165
|
+
policy reference and version. Changing any covered value needs a new approval.
|
|
166
|
+
`parse_request_json` refuses duplicate keys, floats and non-finite numbers.
|
|
167
|
+
- **Who decides.** The app's `ApproverDirectory` names the owner-designated
|
|
168
|
+
approver for the policy; the requester never chooses the approver, the
|
|
169
|
+
contact or the delivery. The requester sees only `RequesterView`
|
|
170
|
+
(`request_id`, state, delivery status, outcome): never a link, token or
|
|
171
|
+
capability.
|
|
172
|
+
- **Presentation.** A link presentation sends a one-use link to the approver's
|
|
173
|
+
registered contact through the app's `ApprovalDelivery`; only its SHA-256
|
|
174
|
+
verifier is stored. The link only locates the request. Opening it consumes
|
|
175
|
+
nothing; a successful decision uses it. Under a passkey-minimum policy the
|
|
176
|
+
request is shown only after a `view` assertion, which consumes nothing;
|
|
177
|
+
otherwise, or when the policy sets `link_reveals_request`, link possession
|
|
178
|
+
alone reveals the request. A task presentation needs the app's
|
|
179
|
+
`TaskAnswerVerifier` to verify the member-signed answer and its receipt, and
|
|
180
|
+
the authenticator proof is bound to that task answer; the approval stays
|
|
181
|
+
pending until both verify.
|
|
182
|
+
- **Assurance.** Each `ActionPolicy` sets a minimum of `member_session` or
|
|
183
|
+
`passkey`. An `Authenticator` adapter derives the evidence from a verified
|
|
184
|
+
proof; a caller cannot claim an assurance by naming it. The passkey adapter is
|
|
185
|
+
below; `member_session` is an adapter seam for apps that have member sessions.
|
|
186
|
+
Every proof is bound by `challenge_binding` to one approval, purpose
|
|
187
|
+
(approve, reject or view) and task answer.
|
|
188
|
+
- **Current approver.** An approval is only as good as its approver's current
|
|
189
|
+
designation: challenge, decide and admit refuse with `approver_retired` once
|
|
190
|
+
the `ApproverDirectory` no longer names the approver.
|
|
191
|
+
- **Admission.** `admit()` is a compare-and-set under a row lock and a lock per
|
|
192
|
+
request digest. A retry under the same operation ID retrieves the admission
|
|
193
|
+
and is not a new dispatch; after `not_sent` it allows a transport retry while
|
|
194
|
+
the approval is unexpired and unrevoked. The outcome lives on the consumption:
|
|
195
|
+
`dispatching → completed | not_sent | outcome_unknown`, and the owner
|
|
196
|
+
reconciles `outcome_unknown`. While a dispatch of the same request digest is
|
|
197
|
+
dispatching or unknown, no other approval of it is admitted. No outcome rearms
|
|
198
|
+
an approval.
|
|
199
|
+
- **Transactions.** `decide`, `admit`, `record_outcome`, `reconcile`, `revoke`
|
|
200
|
+
and `revoke_many` take an optional pgdbm transaction from the same database,
|
|
201
|
+
so the change commits with the app's own writes. Audit events go to the app's
|
|
202
|
+
`AuditSink` inside the transaction of the change they record.
|
|
203
|
+
- **Storage.** `ApprovalStore` is the interface; `PgApprovalStore(db)` keeps
|
|
204
|
+
the records in the app's own pgdbm schema, and `migrate()` applies its
|
|
205
|
+
migrations (module `aweb_naapp_approvals`).
|
|
206
|
+
|
|
207
|
+
`aweb_naapp.runtime.passkeys` verifies app-local WebAuthn passkeys with
|
|
208
|
+
[py_webauthn](https://github.com/duo-labs/py_webauthn), maintained by Duo Labs
|
|
209
|
+
since 2017. It does all of the WebAuthn cryptography and
|
|
210
|
+
parsing: attestation formats, COSE keys, signature and counter checks. Its
|
|
211
|
+
server-only API takes the expected challenge, relying party ID, origin and
|
|
212
|
+
user-verification requirement as explicit arguments, which this module binds to
|
|
213
|
+
the approval, decision, policy and the app's own origin.
|
|
214
|
+
|
|
215
|
+
- The relying party is the app's own origin (`RelyingParty`).
|
|
216
|
+
- `verify_registration(credential, challenge=..., rp=...)` and
|
|
217
|
+
`verify_assertion(proof, challenge=..., rp=..., credential_id=...,
|
|
218
|
+
public_key=..., sign_count=...)` are the stateless ceremony checks `Passkeys`
|
|
219
|
+
uses, for any caller with its own challenges and credentials. They always
|
|
220
|
+
require user verification and raise `WebAuthnRefused` on any failure.
|
|
221
|
+
`verify_assertion` takes the stored credential the caller chose for the
|
|
222
|
+
expected subject and refuses a proof naming any other credential, since the
|
|
223
|
+
proof's credential ID is not signed. The caller makes each challenge
|
|
224
|
+
single-use, stores the credential and advances its counter with a
|
|
225
|
+
compare-and-set on `new_sign_count`.
|
|
226
|
+
- `designate(tx, scope=..., contact=..., evidence=...)` runs in the caller's
|
|
227
|
+
transaction. The evidence is a `VerifiedOwnerAct`, which the owner-act
|
|
228
|
+
verifier (`aweb_naapp.hooks.owner_act_nonces`) consumed in that same
|
|
229
|
+
transaction: `app-role.designate` for a scope's first approver,
|
|
230
|
+
`app-credential.recover` to replace one. The act names the scope, commits to
|
|
231
|
+
the contact by hash and takes effect once. For a scope the app does not count
|
|
232
|
+
as an owner scope (`Passkeys(owner_scopes=...)`, every scope by default), the
|
|
233
|
+
evidence may instead be an `OwnerApproval` from `owner_approval()`: an owner's
|
|
234
|
+
decided approval, admitted and completed through B2 in the same transaction.
|
|
235
|
+
It designates only a scope's first approver. Whether the approved request asks
|
|
236
|
+
for that designation is the app's check.
|
|
237
|
+
- Nothing is sent inside the transaction. After the commit the caller sends the
|
|
238
|
+
enrolment link and any recovery notices with `deliver(designation)`. If that
|
|
239
|
+
never happens, `reissue_enrolment(tx, scope=...)` revokes the unused link and
|
|
240
|
+
issues a new one to the same contact, while the approver has not enrolled.
|
|
241
|
+
- The enrolment link goes only to the designated contact. Whoever first
|
|
242
|
+
completes enrolment through the link registers the passkey: this is **trust
|
|
243
|
+
on first use**, and the audit event records it as such. The link is as strong
|
|
244
|
+
as the contact channel it travels through.
|
|
245
|
+
- Recovery from a lockout retires the previous approver at once, with its
|
|
246
|
+
passkeys, unused enrolment links and live approvals (through
|
|
247
|
+
`ApprovalRevocation`, in the same transaction). The new approver takes effect
|
|
248
|
+
after a cooling-off period (`LockoutPolicy`, at least 72 h), during which
|
|
249
|
+
the scope has no approver, and every contact previously recorded for the
|
|
250
|
+
scope is notified. `recovery_pending_until` tells the app to freeze its own
|
|
251
|
+
widening changes meanwhile.
|
|
252
|
+
- `retire(tx, scope=..., evidence=...)` ends a scope's designation on an
|
|
253
|
+
`app-role.revoke` act consumed in the same transaction, for instance when the
|
|
254
|
+
owner role behind the scope is revoked. The act's `subject_ref` must name the
|
|
255
|
+
approver retire finds current, so a recovery committed after the act was
|
|
256
|
+
signed is never retired by it. The current approver, or a recovery's
|
|
257
|
+
pending one, is retired with its passkeys, unused enrolment links and live
|
|
258
|
+
approvals. The scope then has no approver: its next designation is a first
|
|
259
|
+
one and enrols a new passkey, and the retired passkeys stay dead.
|
|
260
|
+
- A contact change needs the approver's own passkey, takes effect after a delay
|
|
261
|
+
(at least 24 h) and is notified to the old and the new contact.
|
|
262
|
+
- These durations are floors. App or owner policy may lengthen them, and
|
|
263
|
+
deploy configuration may raise the floor itself (`lockout_floor`); nothing
|
|
264
|
+
can go below 72 h and 24 h.
|
|
265
|
+
- Assertions are bound to one approval, purpose, task answer, request digest
|
|
266
|
+
and policy version through `challenge_binding`. They need user verification, are
|
|
267
|
+
single-use, and the authenticator counter must advance.
|
|
268
|
+
- Designation is the owner's attestation of one natural person. The primitive
|
|
269
|
+
cannot detect sharing: a passkey or contact that several people share makes
|
|
270
|
+
every approval attributable only to the group. Every `Authenticator` must
|
|
271
|
+
likewise refuse non-individual principals; a `member_session` adapter needs a
|
|
272
|
+
per-member principal from the identity layer or a custody attestation.
|
|
273
|
+
- `login_options(scope)` / `verify_login(scope, proof)` log in a scope's
|
|
274
|
+
in-effect approver under the same rules, returning `PasskeyLogin(subject,
|
|
275
|
+
contact, evidence_ref)`. What a login opens (a console or reader session) is
|
|
276
|
+
the caller's. Roles are named scopes, such as `secrets.owner`, each designated
|
|
277
|
+
through a team owner act.
|
|
278
|
+
- A passkey proves that an app-local human holds the enrolled authenticator. It
|
|
279
|
+
is not AWID membership, team identity or key custody.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# aweb-naapp
|
|
2
|
+
|
|
3
|
+
The reusable Native Agentic App (naapp) docs surface. Every naapp built with it
|
|
4
|
+
serves the same standard surface — so the standard is shared code, not a doc
|
|
5
|
+
copied by hand.
|
|
6
|
+
|
|
7
|
+
An app supplies its canonical `aweb-app.json` manifest, a `SiteConfig` (brand,
|
|
8
|
+
nav, footer), and its own landing body, and gets:
|
|
9
|
+
|
|
10
|
+
- the **aweb design system** (Paper/Clay), vendored and sha-pinned, served at
|
|
11
|
+
`/css/aweb.css`;
|
|
12
|
+
- the **shared chrome** — head, header, footer, theme toggle, and copy button;
|
|
13
|
+
- **manifest-driven llms.txt blocks** — the operations list and the
|
|
14
|
+
team-certificate authentication section;
|
|
15
|
+
- the **`/reference` page** — every operation in parallel as the canonical
|
|
16
|
+
`aw <verb>` command and the raw HTTP wire format, with the honest team-cert v2
|
|
17
|
+
envelope linked to the conformance vector.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import aweb_naapp as naapp
|
|
21
|
+
|
|
22
|
+
site = naapp.SiteConfig(
|
|
23
|
+
origin="https://library.aweb.ai",
|
|
24
|
+
brand="library",
|
|
25
|
+
title="library — agent profiles for AWID teams",
|
|
26
|
+
description="...",
|
|
27
|
+
nav_links=(naapp.NavLink("Reference", "/reference"), ...),
|
|
28
|
+
footer_blurb="...",
|
|
29
|
+
footer_columns=(...),
|
|
30
|
+
footer_bottom="library is a Native Agentic App on the aweb.ai hub. AWID is the identity authority.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
reference_html = naapp.render_reference(manifest, site, verb="library")
|
|
34
|
+
landing_html = naapp.page(site, body_html, include_copy_llms=True)
|
|
35
|
+
css_bytes = naapp.aweb_css()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The docs surface is pure standard library. Request-time runtime code (the
|
|
39
|
+
team-auth verifier, approvals, passkeys, and team-server discovery from the
|
|
40
|
+
team's AWID `aweb.team-server/1` record) lives under `aweb_naapp.runtime` and
|
|
41
|
+
needs the `runtime` extra: `aweb-naapp[runtime]`. Two parts the core server
|
|
42
|
+
shares import without it: the durable outbox, `aweb_naapp.outbox`
|
|
43
|
+
(`aweb-naapp[outbox]`), and the signing-key reader, `aweb_naapp.signing_key`
|
|
44
|
+
(`aweb-naapp[signing-key]`).
|
|
45
|
+
|
|
46
|
+
The egress guard, `aweb_naapp.egress`, is for naapps that call an external
|
|
47
|
+
service with their own credential. It needs only the `egress` extra,
|
|
48
|
+
`aweb-naapp[egress]`, and none of the runtime. It canonicalizes and matches
|
|
49
|
+
outbound URLs under the contracts in `docs/vectors/egress-url-v2.json` and
|
|
50
|
+
`docs/vectors/egress-address-v1.json`, resolves names itself and refuses any
|
|
51
|
+
denied address, and sends a request only to the destination it matches,
|
|
52
|
+
through one pinned, verified connection. Redirects come back as responses, no proxy is read from
|
|
53
|
+
the environment, and a failure is classed as not sent or of unknown outcome.
|
|
54
|
+
|
|
55
|
+
### App keys
|
|
56
|
+
|
|
57
|
+
An app names its signing keys by kid, as key files:
|
|
58
|
+
`<PREFIX>APP_KEY_FILES='{"<kid>": "/run/secrets/<file>"}'`. A key file holds the
|
|
59
|
+
32-byte Ed25519 seed as 64 hex characters, with one trailing newline allowed. It
|
|
60
|
+
must be a regular file of at most 1 KiB, and must not be group- or world-writable. The manifest
|
|
61
|
+
declares which kid signs which purpose, and the app does not start unless the
|
|
62
|
+
configured key is the declared one. An app with `<PREFIX>APP_EVENTS_ORIGIN` set
|
|
63
|
+
(https, or http on loopback) must hold its declared app-event key. That key may instead be given as
|
|
64
|
+
`<PREFIX>APP_EMIT_KEY_SEED_HEX` with `<PREFIX>APP_EMIT_KID`, a form that signs
|
|
65
|
+
app events only.
|
|
66
|
+
|
|
67
|
+
The egress guard, `aweb_naapp.egress`, is for naapps that call an external
|
|
68
|
+
service with their own credential. It needs only the `egress` extra,
|
|
69
|
+
`aweb-naapp[egress]`, and none of the runtime. It canonicalizes and matches
|
|
70
|
+
outbound URLs under the contracts in `docs/vectors/egress-url-v2.json` and
|
|
71
|
+
`docs/vectors/egress-address-v1.json`, resolves names itself and refuses any
|
|
72
|
+
denied address, and sends a request only to the destination it matches,
|
|
73
|
+
through one pinned, verified connection. Redirects come back as responses, no proxy is read from
|
|
74
|
+
the environment, and a failure is classed as not sent or of unknown outcome.
|
|
75
|
+
|
|
76
|
+
### Health
|
|
77
|
+
|
|
78
|
+
`/live` reports the app's id and build commit. `/health` and `/ready` also run
|
|
79
|
+
the app's dependency checks and report each by name: `ready` is 503 when any
|
|
80
|
+
check fails, and `health` always answers 200. The runtime checks its database
|
|
81
|
+
and reports the age of its oldest undelivered outbox event
|
|
82
|
+
(`NaappRuntime.health_checks()`). An app adds its own as async callables that
|
|
83
|
+
return a `CheckResult`. A check that raises, or does not answer within 5 s,
|
|
84
|
+
fails, and its error is logged, never served. `/ready` serves only whether each
|
|
85
|
+
check passed; `/health` adds each check's detail, whose values are numbers,
|
|
86
|
+
booleans or null, never text. The routes are unauthenticated,
|
|
87
|
+
so the checks run at most once every 10 s: concurrent requests share the run in
|
|
88
|
+
flight, and later ones get its result until it expires.
|
|
89
|
+
|
|
90
|
+
### Agent and human origins
|
|
91
|
+
|
|
92
|
+
An app with pages for people in a browser serves them on their own origin.
|
|
93
|
+
`aweb_naapp.runtime.origins.split_origins(agent_app, human_app, agent_origin=...,
|
|
94
|
+
human_origin=...)` is one ASGI app that dispatches on `Host`:
|
|
95
|
+
|
|
96
|
+
- the agent origin serves the runtime's app, with no cookies in either
|
|
97
|
+
direction, so its only authority is the team-auth signature;
|
|
98
|
+
- the human origin refuses any request carrying `Authorization`, `X-AWEB-*` or
|
|
99
|
+
`X-AWID-*` (`403 team_auth_refused`);
|
|
100
|
+
- any other host is `421 unknown_origin`.
|
|
101
|
+
|
|
102
|
+
A route exists on one origin because it is declared in one of the two apps. The
|
|
103
|
+
lifespan is the agent app's.
|
|
104
|
+
|
|
105
|
+
## Approvals and passkeys
|
|
106
|
+
|
|
107
|
+
`aweb_naapp.runtime.approvals` is the single-use human approval primitive that
|
|
108
|
+
apps share. An agent proposes an exact request; the owner-designated human
|
|
109
|
+
approves or rejects it; a trusted dispatcher then admits exactly one dispatch of
|
|
110
|
+
that exact request.
|
|
111
|
+
|
|
112
|
+
- **Frozen request.** `request_sha256` is SHA-256 over
|
|
113
|
+
`aweb.approval-request/1\n` plus the canonical JSON of the request and its
|
|
114
|
+
policy reference and version. Changing any covered value needs a new approval.
|
|
115
|
+
`parse_request_json` refuses duplicate keys, floats and non-finite numbers.
|
|
116
|
+
- **Who decides.** The app's `ApproverDirectory` names the owner-designated
|
|
117
|
+
approver for the policy; the requester never chooses the approver, the
|
|
118
|
+
contact or the delivery. The requester sees only `RequesterView`
|
|
119
|
+
(`request_id`, state, delivery status, outcome): never a link, token or
|
|
120
|
+
capability.
|
|
121
|
+
- **Presentation.** A link presentation sends a one-use link to the approver's
|
|
122
|
+
registered contact through the app's `ApprovalDelivery`; only its SHA-256
|
|
123
|
+
verifier is stored. The link only locates the request. Opening it consumes
|
|
124
|
+
nothing; a successful decision uses it. Under a passkey-minimum policy the
|
|
125
|
+
request is shown only after a `view` assertion, which consumes nothing;
|
|
126
|
+
otherwise, or when the policy sets `link_reveals_request`, link possession
|
|
127
|
+
alone reveals the request. A task presentation needs the app's
|
|
128
|
+
`TaskAnswerVerifier` to verify the member-signed answer and its receipt, and
|
|
129
|
+
the authenticator proof is bound to that task answer; the approval stays
|
|
130
|
+
pending until both verify.
|
|
131
|
+
- **Assurance.** Each `ActionPolicy` sets a minimum of `member_session` or
|
|
132
|
+
`passkey`. An `Authenticator` adapter derives the evidence from a verified
|
|
133
|
+
proof; a caller cannot claim an assurance by naming it. The passkey adapter is
|
|
134
|
+
below; `member_session` is an adapter seam for apps that have member sessions.
|
|
135
|
+
Every proof is bound by `challenge_binding` to one approval, purpose
|
|
136
|
+
(approve, reject or view) and task answer.
|
|
137
|
+
- **Current approver.** An approval is only as good as its approver's current
|
|
138
|
+
designation: challenge, decide and admit refuse with `approver_retired` once
|
|
139
|
+
the `ApproverDirectory` no longer names the approver.
|
|
140
|
+
- **Admission.** `admit()` is a compare-and-set under a row lock and a lock per
|
|
141
|
+
request digest. A retry under the same operation ID retrieves the admission
|
|
142
|
+
and is not a new dispatch; after `not_sent` it allows a transport retry while
|
|
143
|
+
the approval is unexpired and unrevoked. The outcome lives on the consumption:
|
|
144
|
+
`dispatching → completed | not_sent | outcome_unknown`, and the owner
|
|
145
|
+
reconciles `outcome_unknown`. While a dispatch of the same request digest is
|
|
146
|
+
dispatching or unknown, no other approval of it is admitted. No outcome rearms
|
|
147
|
+
an approval.
|
|
148
|
+
- **Transactions.** `decide`, `admit`, `record_outcome`, `reconcile`, `revoke`
|
|
149
|
+
and `revoke_many` take an optional pgdbm transaction from the same database,
|
|
150
|
+
so the change commits with the app's own writes. Audit events go to the app's
|
|
151
|
+
`AuditSink` inside the transaction of the change they record.
|
|
152
|
+
- **Storage.** `ApprovalStore` is the interface; `PgApprovalStore(db)` keeps
|
|
153
|
+
the records in the app's own pgdbm schema, and `migrate()` applies its
|
|
154
|
+
migrations (module `aweb_naapp_approvals`).
|
|
155
|
+
|
|
156
|
+
`aweb_naapp.runtime.passkeys` verifies app-local WebAuthn passkeys with
|
|
157
|
+
[py_webauthn](https://github.com/duo-labs/py_webauthn), maintained by Duo Labs
|
|
158
|
+
since 2017. It does all of the WebAuthn cryptography and
|
|
159
|
+
parsing: attestation formats, COSE keys, signature and counter checks. Its
|
|
160
|
+
server-only API takes the expected challenge, relying party ID, origin and
|
|
161
|
+
user-verification requirement as explicit arguments, which this module binds to
|
|
162
|
+
the approval, decision, policy and the app's own origin.
|
|
163
|
+
|
|
164
|
+
- The relying party is the app's own origin (`RelyingParty`).
|
|
165
|
+
- `verify_registration(credential, challenge=..., rp=...)` and
|
|
166
|
+
`verify_assertion(proof, challenge=..., rp=..., credential_id=...,
|
|
167
|
+
public_key=..., sign_count=...)` are the stateless ceremony checks `Passkeys`
|
|
168
|
+
uses, for any caller with its own challenges and credentials. They always
|
|
169
|
+
require user verification and raise `WebAuthnRefused` on any failure.
|
|
170
|
+
`verify_assertion` takes the stored credential the caller chose for the
|
|
171
|
+
expected subject and refuses a proof naming any other credential, since the
|
|
172
|
+
proof's credential ID is not signed. The caller makes each challenge
|
|
173
|
+
single-use, stores the credential and advances its counter with a
|
|
174
|
+
compare-and-set on `new_sign_count`.
|
|
175
|
+
- `designate(tx, scope=..., contact=..., evidence=...)` runs in the caller's
|
|
176
|
+
transaction. The evidence is a `VerifiedOwnerAct`, which the owner-act
|
|
177
|
+
verifier (`aweb_naapp.hooks.owner_act_nonces`) consumed in that same
|
|
178
|
+
transaction: `app-role.designate` for a scope's first approver,
|
|
179
|
+
`app-credential.recover` to replace one. The act names the scope, commits to
|
|
180
|
+
the contact by hash and takes effect once. For a scope the app does not count
|
|
181
|
+
as an owner scope (`Passkeys(owner_scopes=...)`, every scope by default), the
|
|
182
|
+
evidence may instead be an `OwnerApproval` from `owner_approval()`: an owner's
|
|
183
|
+
decided approval, admitted and completed through B2 in the same transaction.
|
|
184
|
+
It designates only a scope's first approver. Whether the approved request asks
|
|
185
|
+
for that designation is the app's check.
|
|
186
|
+
- Nothing is sent inside the transaction. After the commit the caller sends the
|
|
187
|
+
enrolment link and any recovery notices with `deliver(designation)`. If that
|
|
188
|
+
never happens, `reissue_enrolment(tx, scope=...)` revokes the unused link and
|
|
189
|
+
issues a new one to the same contact, while the approver has not enrolled.
|
|
190
|
+
- The enrolment link goes only to the designated contact. Whoever first
|
|
191
|
+
completes enrolment through the link registers the passkey: this is **trust
|
|
192
|
+
on first use**, and the audit event records it as such. The link is as strong
|
|
193
|
+
as the contact channel it travels through.
|
|
194
|
+
- Recovery from a lockout retires the previous approver at once, with its
|
|
195
|
+
passkeys, unused enrolment links and live approvals (through
|
|
196
|
+
`ApprovalRevocation`, in the same transaction). The new approver takes effect
|
|
197
|
+
after a cooling-off period (`LockoutPolicy`, at least 72 h), during which
|
|
198
|
+
the scope has no approver, and every contact previously recorded for the
|
|
199
|
+
scope is notified. `recovery_pending_until` tells the app to freeze its own
|
|
200
|
+
widening changes meanwhile.
|
|
201
|
+
- `retire(tx, scope=..., evidence=...)` ends a scope's designation on an
|
|
202
|
+
`app-role.revoke` act consumed in the same transaction, for instance when the
|
|
203
|
+
owner role behind the scope is revoked. The act's `subject_ref` must name the
|
|
204
|
+
approver retire finds current, so a recovery committed after the act was
|
|
205
|
+
signed is never retired by it. The current approver, or a recovery's
|
|
206
|
+
pending one, is retired with its passkeys, unused enrolment links and live
|
|
207
|
+
approvals. The scope then has no approver: its next designation is a first
|
|
208
|
+
one and enrols a new passkey, and the retired passkeys stay dead.
|
|
209
|
+
- A contact change needs the approver's own passkey, takes effect after a delay
|
|
210
|
+
(at least 24 h) and is notified to the old and the new contact.
|
|
211
|
+
- These durations are floors. App or owner policy may lengthen them, and
|
|
212
|
+
deploy configuration may raise the floor itself (`lockout_floor`); nothing
|
|
213
|
+
can go below 72 h and 24 h.
|
|
214
|
+
- Assertions are bound to one approval, purpose, task answer, request digest
|
|
215
|
+
and policy version through `challenge_binding`. They need user verification, are
|
|
216
|
+
single-use, and the authenticator counter must advance.
|
|
217
|
+
- Designation is the owner's attestation of one natural person. The primitive
|
|
218
|
+
cannot detect sharing: a passkey or contact that several people share makes
|
|
219
|
+
every approval attributable only to the group. Every `Authenticator` must
|
|
220
|
+
likewise refuse non-individual principals; a `member_session` adapter needs a
|
|
221
|
+
per-member principal from the identity layer or a custody attestation.
|
|
222
|
+
- `login_options(scope)` / `verify_login(scope, proof)` log in a scope's
|
|
223
|
+
in-effect approver under the same rules, returning `PasskeyLogin(subject,
|
|
224
|
+
contact, evidence_ref)`. What a login opens (a console or reader session) is
|
|
225
|
+
the caller's. Roles are named scopes, such as `secrets.owner`, each designated
|
|
226
|
+
through a team owner act.
|
|
227
|
+
- A passkey proves that an app-local human holds the enrolled authenticator. It
|
|
228
|
+
is not AWID membership, team identity or key custody.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aweb-naapp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Reusable Native Agentic App (naapp) docs surface: shared chrome, the aweb design system, and manifest-driven llms.txt and reference generators."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "aweb.ai", email = "hello@aweb.ai" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = ["agents", "awid", "aweb", "naapp", "manifest"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
# Request-time runtime code under aweb_naapp.runtime. The docs
|
|
22
|
+
# surface (`import aweb_naapp`) never imports these.
|
|
23
|
+
runtime = [
|
|
24
|
+
# The first awid release with the principal-attestation records the runtime reads.
|
|
25
|
+
"awid-service>=0.5.30",
|
|
26
|
+
"fastapi>=0.116.1",
|
|
27
|
+
"httpx>=0.28.1",
|
|
28
|
+
"pgdbm>=0.5.0",
|
|
29
|
+
"pydantic-settings>=2.10.1",
|
|
30
|
+
"pynacl>=1.6.2",
|
|
31
|
+
"webauthn>=3.0.1",
|
|
32
|
+
]
|
|
33
|
+
# The shared durable outbox, aweb_naapp.outbox: pgdbm only, so the core server
|
|
34
|
+
# can use it without the naapp runtime.
|
|
35
|
+
outbox = [
|
|
36
|
+
"pgdbm>=0.5.0",
|
|
37
|
+
]
|
|
38
|
+
# The signing-key reader, aweb_naapp.signing_key: pynacl only, so the core
|
|
39
|
+
# server can read its keys the same way without the naapp runtime.
|
|
40
|
+
signing-key = [
|
|
41
|
+
"pynacl>=1.6.2",
|
|
42
|
+
]
|
|
43
|
+
# The shared dispatch state machine, aweb_naapp.dispatch: pgdbm only.
|
|
44
|
+
dispatch = [
|
|
45
|
+
"pgdbm>=0.5.0",
|
|
46
|
+
]
|
|
47
|
+
# The shared idempotency store, aweb_naapp.idempotency: pgdbm only.
|
|
48
|
+
idempotency = [
|
|
49
|
+
"pgdbm>=0.5.0",
|
|
50
|
+
]
|
|
51
|
+
# Shared role assignments, aweb_naapp.roles: pgdbm only.
|
|
52
|
+
roles = [
|
|
53
|
+
"pgdbm>=0.5.0",
|
|
54
|
+
]
|
|
55
|
+
# The shared page-format validator, aweb_naapp.pages: JSON Schema only, no I/O.
|
|
56
|
+
pages = [
|
|
57
|
+
"jsonschema>=4.23",
|
|
58
|
+
]
|
|
59
|
+
# The shared egress guard, aweb_naapp.egress: URL canonicalization, its own
|
|
60
|
+
# resolver and a pinned verified connection, without the naapp runtime.
|
|
61
|
+
# publicsuffixlist is pinned exactly on purpose: the list decides which
|
|
62
|
+
# wildcard destinations are refused, so it changes only by a reviewed bump of
|
|
63
|
+
# this pin together with docs/vectors/egress-url-v2.json. Do not loosen it.
|
|
64
|
+
egress = [
|
|
65
|
+
"dnspython>=2.7.0",
|
|
66
|
+
"httpcore[asyncio]>=1.0.9",
|
|
67
|
+
"idna>=3.10",
|
|
68
|
+
"publicsuffixlist==1.1.0.20261018",
|
|
69
|
+
]
|
|
70
|
+
# aw-hooks, aweb_naapp.hooks: the catalog reader (stdlib), the event envelope,
|
|
71
|
+
# the delivery credential and the owner-act verifier with its nonce table.
|
|
72
|
+
# It shares awid's did:key and canonical-JSON primitives and pgdbm, never the
|
|
73
|
+
# naapp runtime: importing it loads no web framework, settings or HTTP client
|
|
74
|
+
# (a subprocess test pins that). Installing awid-service brings that package's
|
|
75
|
+
# own dependencies, which the core server and every naapp already carry.
|
|
76
|
+
hooks = [
|
|
77
|
+
"awid-service>=0.5.30",
|
|
78
|
+
"pgdbm>=0.5.0",
|
|
79
|
+
"pynacl>=1.6.2",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Persistent pages need PostgreSQL and the frozen validator, not HTTP/auth deps.
|
|
83
|
+
page-store = [
|
|
84
|
+
"pgdbm>=0.5.0",
|
|
85
|
+
"jsonschema>=4.23",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[project.urls]
|
|
89
|
+
Homepage = "https://github.com/awebai/aweb/tree/main/naapp-lib"
|
|
90
|
+
Repository = "https://github.com/awebai/aweb"
|
|
91
|
+
Issues = "https://github.com/awebai/aweb/issues"
|
|
92
|
+
|
|
93
|
+
[build-system]
|
|
94
|
+
requires = ["hatchling"]
|
|
95
|
+
build-backend = "hatchling.build"
|
|
96
|
+
|
|
97
|
+
[tool.hatch.build.targets.wheel]
|
|
98
|
+
packages = ["src/aweb_naapp"]
|
|
99
|
+
|
|
100
|
+
[tool.uv.sources]
|
|
101
|
+
awid-service = { path = "../awid", editable = true }
|
|
102
|
+
aweb-naapp-testing = { path = "testing", editable = true }
|
|
103
|
+
|
|
104
|
+
[dependency-groups]
|
|
105
|
+
dev = [
|
|
106
|
+
"aweb-naapp[runtime,pages]",
|
|
107
|
+
"aweb-naapp[egress]",
|
|
108
|
+
"cryptography>=44.0.0",
|
|
109
|
+
"aweb-naapp-testing",
|
|
110
|
+
"pytest>=9.0.0",
|
|
111
|
+
"pytest-asyncio>=1.2.0",
|
|
112
|
+
"httpx2>=2.9.1",
|
|
113
|
+
"ruff>=0.14.0",
|
|
114
|
+
"mypy>=1.18.0",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
pythonpath = ["src"]
|
|
119
|
+
testpaths = ["tests"]
|
|
120
|
+
asyncio_mode = "auto"
|
|
121
|
+
filterwarnings = ["error"]
|
|
122
|
+
|
|
123
|
+
[tool.ruff]
|
|
124
|
+
target-version = "py312"
|
|
125
|
+
line-length = 100
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint]
|
|
128
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
129
|
+
ignore = ["E501"]
|
|
130
|
+
|
|
131
|
+
[tool.mypy]
|
|
132
|
+
python_version = "3.12"
|
|
133
|
+
mypy_path = "src"
|
|
134
|
+
ignore_missing_imports = true
|
|
135
|
+
warn_unused_configs = true
|