@astram/guide 0.2.23 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/CAPABILITIES.md +57 -9
  2. package/package.json +1 -1
package/CAPABILITIES.md CHANGED
@@ -625,10 +625,34 @@ upload(@Req() req: Record<string, unknown>) {
625
625
  name: multer reports it as an unexpected file), `empty`, `bad_request` (malformed or truncated
626
626
  multipart, raw control characters in part headers); 413 `too_large`; 415 `type_not_allowed`,
627
627
  `content_mismatch`. Raw UTF-8 and RFC 5987 (`filename*=UTF-8''…`) file names decode to the
628
- same name. Size: the route cap defaults to the profile's configured cap
629
- (`OBJECT_STORAGE_MAX_BYTES` under `storage: 'env'`, else 10 MiB); `maxBytes` can only lower it.
630
- Profiles: `xlsx` only (50 MiB hard ceiling for any profile, route or env value). Needs
628
+ same name. Size: the route cap defaults to the profile's configured cap (below); `maxBytes` can
629
+ only lower it. A route naming a profile the app did not register aborts boot. Needs
631
630
  `@nestjs/platform-express` (an optional peer; the scaffold has it).
631
+ - **Profiles** — turn them on with `ShieldModule.forRoot({ fileProfiles: ['xlsx', 'csv', 'pdf', 'docx'] })`
632
+ (default `['xlsx']`; an unknown, repeated or empty list aborts boot). You cannot define your own.
633
+ | Profile | Default cap | Declared types accepted (stored as the first) | Content check |
634
+ |---|---|---|---|
635
+ | `xlsx` | 10 MiB | spreadsheetml `.sheet` | ZIP + `[Content_Types].xml` names a spreadsheetml main part |
636
+ | `csv` | 10 MiB | `text/csv`, `application/csv`, `text/plain`, `application/vnd.ms-excel` | valid UTF-8 (BOM allowed), no NUL byte, not blank |
637
+ | `pdf` | 20 MiB | `application/pdf`, `application/x-pdf` | `%PDF-` in the first KiB, `%%EOF` in the last KiB, not encrypted (`/Encrypt`) |
638
+ | `docx` | 10 MiB | wordprocessingml `.document` (`.docx` only, not `.docm`) | ZIP + `[Content_Types].xml` names a wordprocessingml main part |
639
+
640
+ Caps: when `OBJECT_STORAGE_MAX_BYTES` is set (under `storage: 'env'`) it is the cap of every
641
+ registered profile; unset, each keeps its default. 50 MiB is the hard ceiling for any profile,
642
+ route or env value. The stored type is always the profile's first type, whatever allowed type
643
+ the browser declared.
644
+ - **Archive limits (`ZIP_LIMITS`, xlsx and docx)** — read from the zip's central directory before
645
+ anything is decompressed; a breach is 415 `content_mismatch`: at most 10 000 entries, 256 MiB
646
+ declared uncompressed in total, a compression ratio of at most 100 for any entry over 1 MiB,
647
+ methods stored/deflate only, no encrypted entry, no zip64, one `[Content_Types].xml`, a
648
+ well-formed end record and central directory, no entry name with `..`, `\`, a leading `/`, a
649
+ drive letter or a control character, and no nested archive (`.zip`, `.jar`, `.7z`, `.rar`,
650
+ `.gz`, `.tar`, `.xlsm`, `.docm`, `.xlsx`/`.docx`/`.pptx` outside the embeddings folders, or a
651
+ stored entry that is itself a zip).
652
+ Embedded Office objects (`.xlsx`, `.docx`, `.pptx`) are allowed under `word/embeddings/` and
653
+ `xl/embeddings/` (a Word chart keeps its data there); they count toward the limits and are never
654
+ opened. Files Office, LibreOffice, Google and common zip libraries write pass; a file with bytes
655
+ added after the zip's end record (some mail gateways pad attachments) is refused.
632
656
  - **`FilesService`** (provided by `IdentityFeatureModule`) — `store(intake)`, `open(uuid)`,
633
657
  `remove(uuid)`. No actor argument: tenant, owner and audit actor come from the session. `store`
634
658
  puts the object at `t/<tenant>/<uuid>`, then creates the record (gate, tenant stamp, audit); a
@@ -638,7 +662,8 @@ upload(@Req() req: Record<string, unknown>) {
638
662
  your own record that points at the file. Missing and foreign answer the same 403; non-uuid 400.
639
663
  The bytes are re-hashed against the stored sha256. Headers: `Content-Disposition: attachment`
640
664
  (RFC 5987 name), `X-Content-Type-Options: nosniff`, `Cache-Control: no-store`, and
641
- `Content-Type` from the record when it is an allowlisted type, else `application/octet-stream`.
665
+ `Content-Type` from the record when it is the type of a registered profile, else
666
+ `application/octet-stream`.
642
667
  - **Status codes** — storage not configured (no `storage` option or `OBJECT_STORAGE_BUCKET`
643
668
  unset) ⇒ 501 from `FilesService` and the route; store down, stored object missing or sha256
644
669
  mismatch ⇒ 503 with a fixed message.
@@ -649,13 +674,14 @@ upload(@Req() req: Record<string, unknown>) {
649
674
  `OBJECT_STORAGE_ENDPOINT` (`https:`; `http:` only in development/test or to a loopback host),
650
675
  `OBJECT_STORAGE_REGION` (default `us-east-1`), `OBJECT_STORAGE_ACCESS_KEY_ID`,
651
676
  `OBJECT_STORAGE_SECRET_ACCESS_KEY`, `OBJECT_STORAGE_SSE` (`AES256` only),
652
- `OBJECT_STORAGE_MAX_BYTES` (plain decimal bytes, no leading zeros; the per-file cap for the
653
- `xlsx` profile and every `FileIntake` route). Outside development/test the two credentials must be
677
+ `OBJECT_STORAGE_MAX_BYTES` (plain decimal bytes, no leading zeros; when set, the per-file cap
678
+ for every registered profile and so every `FileIntake` route; unset, each profile's default). Outside development/test the two credentials must be
654
679
  `secret-ref:` values resolved at boot; a literal is refused. With `storage: 'env'` the bucket is
655
680
  pinged at boot (probe put/get/delete under `_probe/`); failure aborts boot.
656
- - **Your xlsx parser must cap entry count and total uncompressed size.** The framework checks only
657
- the zip header and `[Content_Types].xml`, not every entry, so a zip bomb in another part passes
658
- the upload check.
681
+ - **Your xlsx/docx parser must still bound what it inflates.** The upload checks the sizes each
682
+ entry declares; only `[Content_Types].xml` is decompressed. A parser must stop when real output
683
+ passes the limits, and must not open embedded objects. Csv row/column and pdf page limits are
684
+ the parser's too.
659
685
  - **Lint wall** — the scaffold bans `multer`, `@aws-sdk/*`, `minio`, `fs` / `node:fs` (and
660
686
  `/promises`) in `src/features/**`: use `FileIntake` and `FilesService`, never the bucket.
661
687
  - Roles that upload need `stored_file:create` (FilesService creates the record as the caller), but app code must create `stored_file` rows only through `FilesService.store()` — never `records.create('stored_file', …)` / `createMany`, which would let a caller set its own `storageKey` (downloads still refuse a key outside the caller's tenant, KI #137).
@@ -831,6 +857,28 @@ upload(@Req() req: Record<string, unknown>) {
831
857
  - **Migrations** 111–113 (identity) add `agent_task`, `agent_action`, `agent_tenant_setting` to
832
858
  `<ns>_identity`. Single layout: re-run `db-provision --layout single` after migrating; check with
833
859
  `db-verify-grants` and the app's layout flags.
860
+ - **Reading attached files (G-037 D2a, opt-in).** `AgentModule.forRoot({ files: { enabled: true } })`
861
+ registers framework capabilities `files.read_sheet@1` (xlsx/csv, ≤ 200 rows a call), `files.profile_sheet@1`
862
+ (per-column type, empty and distinct counts, min/max, top values, computed in code),
863
+ `files.read_pdf_text@1` (text layer, ≤ 10 pages), `files.read_doc_text@1` (docx text, ≤ 20000 chars) and
864
+ `schema.describe@1` (the object types the user can read, never rows). Requires `identity:stored_file:read`.
865
+ - Files enter a conversation as `attachments` (0–8 `stored_file` uuids your own `FileIntake` route returned)
866
+ on `POST /agent/tasks` and on `continue` with a message. The runtime only ever gets task handles; any file
867
+ the user cannot open answers one `404 attachment_unavailable`; a bad list `400 invalid_request`.
868
+ - Every byte is parsed by the isolated analyser `@astram/file-analyser` (Python): never formulas, macros,
869
+ links or embedded objects. Run it as the sidecar container (`docker-compose.analyser.yml` in the
870
+ scaffold: no network, read-only, non-root) or as the hardened systemd unit in the package's `deploy/`.
871
+ Env (read only when `files` is on): `AGENT_FILE_ANALYSER_SOCKET` (required),
872
+ `AGENT_FILE_ANALYSER_TIMEOUT_MS` (12000, 1000–60000).
873
+ - File content is `confidential` and goes only to a model provider the company lists for file content:
874
+ org bootstrap `agent: { mode, fileProviders: [...] }` (migration 114, audited
875
+ `agent.tenant_file_providers.set`). Empty (default) ⇒ no file capability in the manifest. A non-empty
876
+ list is refused outside development/test while the synthetic-data rule stands, and personal-data
877
+ masking (not built yet) is a prerequisite before real data.
878
+ - Totals the assistant states come from `profile_sheet` or `aggregate`, never from adding rows it read.
879
+ Ids inside file content reach the model as `[id]`.
880
+ - Not yet: the attach control in the scaffold UI (the client service supports `attachments`), the
881
+ workbook map (D2b), marking an upload `restricted`.
834
882
 
835
883
  ## Available, but NOT wired to a data path (do not assume they protect data yet)
836
884
  - **Envelope encryption / per-tenant DEKs** (`createEncryptionService`, `LocalKekCrypto`): the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
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",