@astram/guide 0.2.21 → 0.2.23

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.
package/CAPABILITIES.md CHANGED
@@ -239,7 +239,9 @@ imports: [
239
239
  "wire IdentityFeatureModule in AppModule before running bootstrap:org".
240
240
  - **File.** `{ "version": 1, "roots": [{ username, email, homeCompany }], "tenants": [company] }`;
241
241
  `company` = `{ name, departments?, branches?, roles?: [{ name, permissions }], users?:
242
- [{ username, email, company?, postings?: [{ role, department?, branch? }] }], children? }`.
242
+ [{ username, email, company?, postings?: [{ role, department?, branch? }] }], agent?: { mode },
243
+ children? }`. `agent.mode` (`off` | `synthetic`, identity 0.26) sets that company's AI assistant
244
+ mode (below); no `agent` key leaves it unchanged.
243
245
  Unknown fields, unknown permission keys (allowed: catalog keys plus `<feature>:*` and
244
246
  `<feature>:<type>:*`), duplicate usernames or emails and references to undeclared roles,
245
247
  departments or branches are refused before any write. No password, uuid or secret in the file.
@@ -263,6 +265,9 @@ imports: [
263
265
  another company. A posting must use a role declared on its own company (a role declared only on a
264
266
  parent is a file error). After renaming a department or branch in the app, rename it in the file
265
267
  too, or a re-run creates the old name again.
268
+ - **The one overwrite.** A company's `agent.mode` is planned `create`, `unchanged` or `update` (either
269
+ direction); `--apply` writes it with an `agent.tenant_mode.set` audit row. It is the only stored
270
+ value a re-run changes.
266
271
  - **What it does NOT do.** No deletes, renames, moves or revokes (differences are reported only);
267
272
  it never activates an inactive membership or updates a changed email. No upload from the UI.
268
273
  Department and branch admin routes are separate. With `emailSender` configured, `--email-links`
@@ -474,10 +479,25 @@ imports: [
474
479
  (`{ announcement, personal, total }`), `POST /account/notifications/:uuid/read` (204),
475
480
  `POST /account/notifications/read-all?category=` (`{ updated }`). Another user's or company's
476
481
  notice is a 404, like a missing one. Restricted sessions (re-enrolment, mandatory 2FA) get 403.
482
+ - **Notification center (G-035):** the list also takes `kind`, `status=unread|read|archived`
483
+ (absent ⇒ not archived), `from` / `to` (UTC days `YYYY-MM-DD`, inclusive) and `search` (title or
484
+ body, case- and accent-insensitive, ≤ 100 characters; blank ⇒ no search); rows carry
485
+ `kindLabel`, `severity` and `archivedAt`. `GET /account/notifications/kinds` (`{ kinds: [{ kind,
486
+ label, category }] }`), `POST /account/notifications/:uuid/{unread,archive,unarchive}` (204;
487
+ archive also marks read, archived notices are never counted as unread) and `POST
488
+ /account/notifications/bulk` `{ action: read|unread|archive|unarchive, uuids (1–100) }` ⇒
489
+ `{ updated }` (only the caller's own notices in the active company change). Archive never
490
+ keeps a notice past retention. The scaffold's dropdowns show unread notices only and link to
491
+ the `/notifications` page; `server/src/notifications/app-notifications.service.ts` is an example
492
+ for posting your own.
477
493
  - **Register your kinds** once: `ShieldModule.forRoot({ notificationKinds: [{ kind:
478
- 'naes.import_review', category: 'personal' }] })`. A kind is dotted lower-case
479
- (`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`, ≤ 64); `shield.` is reserved. A bad or duplicate entry
480
- fails boot. `category` is `announcement` (release news, notices to many) or `personal`.
494
+ 'naes.import_review', category: 'personal', label: 'Import review', severity: 'warning' }] })`.
495
+ A kind is dotted lower-case (`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`, ≤ 64); `shield.` is
496
+ reserved. A bad or duplicate entry fails boot. `category` is `announcement` (release news,
497
+ notices to many) or `personal`. `label` (optional, 1–40 characters, plain text, shown to every
498
+ company: no company or person names) is the type users see; unset ⇒ made from the kind's last
499
+ segment (`Import review`), cut to 40 with "…", and boot logs one warning listing such kinds.
500
+ `severity` is `info` (default), `success` or `warning`; a bad label or severity fails boot.
481
501
  - **Post** with `NotificationService` (inject `SHIELD_NOTIFICATIONS` from `@astram/guard`):
482
502
  `post({ tenantId, recipients, kind, title, body?, link?, dedupeKey? }, { query? })` ⇒
483
503
  `{ created, skipped }`. `post` trusts its caller: take `tenantId` (and the recipients) from the
@@ -751,6 +771,67 @@ upload(@Req() req: Record<string, unknown>) {
751
771
  grace banner, the Security page (policies), and Users-page 2FA status, sign-in name and admin
752
772
  actions.
753
773
 
774
+ ## AI assistant (`@astram/agent`, G-036 M1, ADR-0049; agent 0.1, core 1.15, engine 0.17, guard 0.28, identity 0.26, frontend-core 0.12)
775
+ - **What it is.** A governed agent layer: the model (in a separate agent runtime) proposes, your
776
+ server executes. The runtime never calls your app; each capability call it asks for runs inside the
777
+ user's own streaming request, as that user, through the record pipe and `decide()`, recorded in
778
+ `agent_action` before it runs. M1: **`read` and `compute` capabilities only**; no workers, no
779
+ writes or proposals (M3), no chat UI in the template, synthetic data only (Rule 8).
780
+ - **Capabilities.** `defineCapability({ id, version, title, description, effect: 'read' |
781
+ 'compute', dataClass: 'internal' | 'confidential' | 'restricted', requires: [keys], input,
782
+ output, limits?, handler, toEvidence?, partial? })`, or `inject` + `factory` for app services.
783
+ `requires` are exact registered keys, all required, re-checked by `decide()` on every call.
784
+ `ctx.records` (`RecordsApi`, engine) is the only data path: writes throw `scope_violation`; a
785
+ `list`/`aggregate` `limit` above `maxRows` is refused (`invalid_args`), absent ⇒ `maxRows`.
786
+ `ctx.handle` / `ctx.resolve` turn uuids into task-scoped handles; uuids in output fail the call.
787
+ `toEvidence` returns `metric` / `table` / `series` evidence for the UI (never sent to the model).
788
+ Validation runs at every boot, also when disabled.
789
+ - **Wiring.** `AgentModule.forRoot({ capabilities, modelProfiles, defaultModelProfile, limits?,
790
+ stepLabels?, allowOpaqueProviders? })` beside `ShieldModule.forRoot({ auth: true })`,
791
+ `IdentityFeatureModule.forRoot` and `EngineModule`. `main.ts`: `NestFactory.create(AppModule,
792
+ { rawBody: true })` (else every agent POST is `503 agent_unavailable`). Registration of the app
793
+ types a capability `requires`: `EngineModule.forRoot` (called inside `IdentityFeatureModule.forRoot`)
794
+ snapshots the `EngineModule.forFeature` registrations at the moment it is called. A `forFeature`
795
+ inside a feature module file's own `@Module` decorator runs when that file is imported, so it is
796
+ always included, wherever that module sits in `imports`. Only an inline
797
+ `EngineModule.forFeature([...])` written in `app.module`'s `imports` array **after**
798
+ `IdentityFeatureModule.forRoot(...)` is missed; `AgentModule` then fails the boot with
799
+ `requires_not_registered`, naming the capability and the key. (The type also goes in
800
+ `extraObjects` so its keys are in the catalog.) An app table
801
+ compared with framework tables (shares, record rules) needs `COLLATE=utf8mb4_unicode_ci`: MariaDB
802
+ 11.4's default `uca1400` collation breaks the engine's share subquery.
803
+ - **Env.** `AGENT_ENABLED` (`0` default: routes answer `403 agent_disabled`, nothing else read),
804
+ `AGENT_RUNTIME_URL` (`https:`, or `http:` in development/test or to a loopback host),
805
+ `AGENT_RUNTIME_KEY_ID`, `AGENT_RUNTIME_KEY` (`secret-ref:` outside development/test).
806
+ - **Who may use it.** Keys `identity:agent:chat` (use it), `identity:agent:restricted_data` (send
807
+ `restricted` data to an approved provider), `identity:agent:propose` (M3). A tenant also needs
808
+ mode `synthetic` (org bootstrap `agent: { mode }`); each company is set on its own. `identity:*`
809
+ holders get all three keys, including `restricted_data` (GAPS #23): grant `identity:agent:chat`
810
+ explicitly on production roles.
811
+ - **Freshness.** Every call re-checks the session (logout, disable, scope switch, restriction), the
812
+ tenant mode, the user's roles and permission version, `identity:agent:chat` and the capability's
813
+ keys. Any grant change in the tenant stops running tasks (`access_revoked`; the user asks again).
814
+ Access-token expiry does not cut a running stream.
815
+ - **Accepted replay edges (M1).** The task's owner can replay its events (`GET :taskId/events`)
816
+ for 10 minutes, also after their access was revoked; a reused `callId` within a task returns the
817
+ earlier recorded result instead of running again (that content already went to the runtime in
818
+ the same task). Both are bounded to the owner's own task and to synthetic data.
819
+ - **Routes** (`/agent/tasks`, session principals only): `POST` open (SSE), `POST :taskId/continue`,
820
+ `POST :taskId/cancel`, `GET :taskId`, `GET :taskId/events` (read-only replay). Disconnect cancels
821
+ the task. Browser client: `createAgentService(http, transport)` in `@astram/frontend-core`;
822
+ `createRefreshingHttp(...).refreshAfterUnauthorized` shares the token refresh with the stream.
823
+ - **Testing.** `@astram/agent/testing`: `createFakeRuntime`, `runCapabilityCase`, `withTenantMode`
824
+ (`NODE_ENV=test`), the shipped synthetic scenarios and `demoCapabilities()`.
825
+ - **Lint.** Add by hand (not in the scaffolded template yet): `agentLintConfig()` from
826
+ `@astram/agent/eslint` (plain flat-config blocks): no model SDK
827
+ in any server file; no `mariadb`, `ioredis`, framework pool or Redis token, `RECORD_ENGINE`,
828
+ `RecordsService` or `ModuleRef` in `*.capability.ts`. ESLint replaces (not merges) a rule's
829
+ options across blocks, so merge `agentRestrictedImports()` into an existing
830
+ `no-restricted-imports` rule for the same files. Exact lists: the `@astram/agent` README.
831
+ - **Migrations** 111–113 (identity) add `agent_task`, `agent_action`, `agent_tenant_setting` to
832
+ `<ns>_identity`. Single layout: re-run `db-provision --layout single` after migrating; check with
833
+ `db-verify-grants` and the app's layout flags.
834
+
754
835
  ## Available, but NOT wired to a data path (do not assume they protect data yet)
755
836
  - **Envelope encryption / per-tenant DEKs** (`createEncryptionService`, `LocalKekCrypto`): the
756
837
  engine exists, but no record field is encrypted and no production `DekStore` ships. Stored
@@ -31,7 +31,7 @@ the current rules, not a stale memory of them. Your project's own rules in
31
31
 
32
32
  The framework packages (`@astram/core`, `@astram/guard`, `@astram/engine`,
33
33
  `@astram/identity`, `@astram/migrate`, `@astram/frontend-core`, `@astram/sdk`,
34
- `@astram/guide`) live in `node_modules` and are **immutable**. Do not edit them, patch
34
+ `@astram/agent`, `@astram/agent-protocol`, `@astram/guide`) live in `node_modules` and are **immutable**. Do not edit them, patch
35
35
  them, monkey-patch them, or reach around their public API. You register your app through
36
36
  the ONE seam — `ShieldModule.forRoot(...)`, plus `IdentityFeatureModule.forRoot(...)` for
37
37
  the identity surface — and consume the exported building blocks. That is the entire
@@ -155,6 +155,35 @@ or identity surface lacks something you need, that's Rule 4 (flag the gap).
155
155
  On any ambiguity or error in a security path — deny. Never "allow on error", never
156
156
  default-open, never swallow a security failure into a success.
157
157
 
158
+ ## 8. AI assistant: capabilities only, through `@astram/agent`
159
+
160
+ The model proposes; your server authorises and executes. The agent runtime (a separate service)
161
+ makes every model call and never calls your app.
162
+
163
+ - **No model SDK in app server code.** No `openai`, `@anthropic-ai/*`, `langchain`, `@langchain/*`,
164
+ `ai`, `@ai-sdk/*`, `@google/genai`, `@google/generative-ai`, `cohere-ai`, `@mistralai/*`,
165
+ `ollama`, `groq-sdk` or `together-ai` import, and no direct LLM HTTP call. Talk to the runtime only
166
+ through `AgentModule.forRoot(...)`; never call the runtime yourself.
167
+ - **Capabilities only via `defineCapability`** (`@astram/agent`), registered in
168
+ `AgentModule.forRoot({ capabilities })`. Never write your own tool route, tool loop or tool
169
+ dispatcher.
170
+ - **A capability reads data only through `ctx.records`**: the request's own records API, so every
171
+ read is the live user's `decide()` and list scope. The `read`/`compute` effect is enforced only
172
+ there; data reached any other way is outside it. In a `*.capability.ts` file never import
173
+ `mariadb`, `ioredis`, a framework pool or Redis token, `RECORD_ENGINE`, `RecordsService` or
174
+ `ModuleRef`. An app that mounts `AgentModule` adds the `agentLintConfig()` rules from
175
+ `@astram/agent/eslint` to `server/eslint.config.mjs`, so lint fails on these imports.
176
+ - **Inject only your own app services** (`inject` + `factory`), typed against `RecordsApi` and
177
+ given `ctx.records` per call. A service that reaches a framework, Nest-internal, request-scoped or
178
+ opaque provider fails the boot; do not work around that check (Rule 4).
179
+ - **Put `total` in a list output.** The model sees only the returned content, so a capability that
180
+ lists rows returns how many exist, not only the page.
181
+ - **No uuids to the model.** Return handles (`ctx.handle(uuid, typeKey)`) and declare handle inputs
182
+ with `"x-astram-handle"`; never put a uuid, id or hex digest in a capability's output.
183
+ - **Synthetic data only.** Enable a tenant (`agent: { mode: "synthetic" }` in the org bootstrap
184
+ file) only when it holds synthetic data. Never set a model profile's `providerApproval.scope` to
185
+ `client_data`; the framework refuses it until real data is approved.
186
+
158
187
  ---
159
188
 
160
189
  *These rules are the framework floor. Your project's rules (in `.astram/guide/`) add to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the AI/dev GUIDE layer (ADR-0025). Ships the framework's hard rules + capabilities manifest as installed, upgrade-refreshed docs. A consumer app's CLAUDE.md references FRAMEWORK_RULES.md from this package; `npm update` replaces it wholesale so new rules arrive cleanly, while the app's own .astram/guide/ layer is never touched. This is the framework-owned FLOOR: an app rule may tighten it, never weaken it.",
6
6
  "type": "commonjs",