@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 +85 -4
- package/FRAMEWORK_RULES.md +30 -1
- package/package.json +1 -1
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? }] }],
|
|
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' }] })`.
|
|
479
|
-
(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`, ≤ 64); `shield.` is
|
|
480
|
-
fails boot. `category` is `announcement` (release news,
|
|
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
|
package/FRAMEWORK_RULES.md
CHANGED
|
@@ -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.
|
|
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",
|