diff --git a/CHANGELOG.md b/CHANGELOG.md index 8379770..f086e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `@intx/*`, `drizzle-orm`, `hono`, `hono-openapi` and `postgres` are peer + dependencies; the host supplies them. `engines` is removed. +- `MEMORY_GRANT_REQUIREMENTS` is read from `package.json` + `interchange.grantRequirements`, now the only declaration. + `MEMORY_CAPABILITY_IDS` is typed `string[]`. + ### Added - Retention HTTP routes (CL-6288): `POST …/memory/documents/:documentId/forget` diff --git a/README.md b/README.md index 19b6c41..1ce4155 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ not ship an answer endpoint. ## Runtime support -Bun >= 1.2 runs the published TypeScript source (`package.json` `exports`); -there is no `dist` build, so native Node does not load it. `engines.node` is -`>=24` as a floor for Node-side tooling (typecheck, pack). +The package ships compiled JavaScript and type declarations in `dist/`, so it +runs on Node 22+ or Bun. -Peer stack you already have on an Interchange hub: `@intx/authz`, -`@intx/hub-api`, `hono`. +Peer dependencies (the stack an Interchange hub already has): `@intx/agent`, +`@intx/authz`, `@intx/hub-api`, `@intx/log`, `@intx/types`, `@intx/workflow`, +`drizzle-orm`, `hono`, `hono-openapi`, `postgres`. ## Quickstart @@ -204,8 +204,8 @@ bun run test # bun test ./src ``` Tests use `createFakeDocumentStore`/`createFakeSourceProvider` (exported for -this purpose) so the suite runs without Postgres. There is no `build` -script — the published surface is `src/`. +this purpose) so the suite runs without Postgres. `bun run build` compiles +`src/` to `dist/`, which is what the package publishes. ## License diff --git a/bun.lock b/bun.lock index f385f95..b9b0906 100644 --- a/bun.lock +++ b/bun.lock @@ -5,21 +5,33 @@ "": { "name": "company-knowledge-engine", "dependencies": { + "arktype": "^2.1.29", + }, + "devDependencies": { + "@intx/agent": "0.4.0", + "@intx/authz": "0.4.0", + "@intx/hub-api": "0.4.0", + "@intx/log": "0.4.0", + "@intx/types": "0.4.0", + "@intx/workflow": "0.4.0", + "@types/bun": "1.4.2", + "drizzle-orm": "0.45.2", + "hono": "4.12.31", + "hono-openapi": "1.3.1", + "postgres": "3.4.9", + "typescript": "^5.9.0", + }, + "peerDependencies": { "@intx/agent": "^0.4.0", "@intx/authz": "^0.4.0", "@intx/hub-api": "^0.4.0", "@intx/log": "^0.4.0", "@intx/types": "^0.4.0", "@intx/workflow": "^0.4.0", - "arktype": "^2.1.29", - "drizzle-orm": "^0.45.1", - "hono": "^4.9.0", + "drizzle-orm": "^0.45.2", + "hono": "^4.12.31", "hono-openapi": "^1.3.1", - "postgres": "^3.4.7", - }, - "devDependencies": { - "@types/bun": "1.4.2", - "typescript": "^5.9.0", + "postgres": "^3.4.9", }, }, }, diff --git a/docs/DISTILLER.md b/docs/DISTILLER.md index 7a76af4..60f538c 100644 --- a/docs/DISTILLER.md +++ b/docs/DISTILLER.md @@ -102,9 +102,9 @@ on a schedule, e.g. from `@corbits/cron`. This package owns no clock. Installer discovery (not live grants): -- `package.json` → `interchange.grantRequirements` -- typed SSOT: `MEMORY_GRANT_REQUIREMENTS` / `MEMORY_CAPABILITY_IDS` from - `@corbits/memory` +- `package.json` → `interchange.grantRequirements` (the single source) +- loaded in-process as `MEMORY_GRANT_REQUIREMENTS` / `MEMORY_CAPABILITY_IDS` + from `@corbits/memory` Minimum capabilities: diff --git a/package.json b/package.json index 81d141f..351c5ca 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,6 @@ }, "license": "LGPL-2.1-only", "type": "module", - "engines": { - "bun": ">=1.2.0", - "node": ">=24" - }, "scripts": { "db:setup": "bun run scripts/db-setup.ts", "typecheck": "tsc --noEmit", @@ -69,20 +65,32 @@ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text ./src" }, "dependencies": { + "arktype": "^2.1.29" + }, + "peerDependencies": { "@intx/agent": "^0.4.0", "@intx/authz": "^0.4.0", "@intx/hub-api": "^0.4.0", "@intx/log": "^0.4.0", "@intx/types": "^0.4.0", "@intx/workflow": "^0.4.0", - "arktype": "^2.1.29", - "drizzle-orm": "^0.45.1", - "hono": "^4.9.0", + "drizzle-orm": "^0.45.2", + "hono": "^4.12.31", "hono-openapi": "^1.3.1", - "postgres": "^3.4.7" + "postgres": "^3.4.9" }, "devDependencies": { + "@intx/agent": "0.4.0", + "@intx/authz": "0.4.0", + "@intx/hub-api": "0.4.0", + "@intx/log": "0.4.0", + "@intx/types": "0.4.0", + "@intx/workflow": "0.4.0", "@types/bun": "1.4.2", + "drizzle-orm": "0.45.2", + "hono": "4.12.31", + "hono-openapi": "1.3.1", + "postgres": "3.4.9", "typescript": "^5.9.0" }, "author": "Sawyer Cutler ", @@ -108,6 +116,7 @@ "README.md", "LICENSE" ], + "sideEffects": false, "publishConfig": { "access": "public" } diff --git a/src/grant-requirements.test.ts b/src/grant-requirements.test.ts index d6a206c..5427dd9 100644 --- a/src/grant-requirements.test.ts +++ b/src/grant-requirements.test.ts @@ -1,7 +1,4 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - import { capabilityIdsForSurface, MEMORY_CAPABILITY_IDS, @@ -79,28 +76,4 @@ describe("MEMORY_GRANT_REQUIREMENTS", () => { "memory:search", ]); }); - - test("package.json interchange.grantRequirements stays in lockstep", () => { - const pkg = JSON.parse( - readFileSync(join(import.meta.dir, "..", "package.json"), "utf8"), - ) as { - interchange?: { - grantRequirements?: Array<{ - resource: string; - action: string; - installHint: string; - surfaces: string[]; - }>; - }; - }; - const fromPkg = pkg.interchange?.grantRequirements ?? []; - expect(fromPkg).toEqual( - MEMORY_GRANT_REQUIREMENTS.map((r) => ({ - resource: r.resource, - action: r.action, - installHint: r.installHint, - surfaces: [...r.surfaces], - })), - ); - }); }); diff --git a/src/grant-requirements.ts b/src/grant-requirements.ts index e1f880c..5f8f75e 100644 --- a/src/grant-requirements.ts +++ b/src/grant-requirements.ts @@ -1,85 +1,65 @@ /** * Grant *requirements* for installers — not live grants. * - * Mirrored under `package.json` → `interchange.grantRequirements` so a - * host installer can read npm metadata without executing code. The typed - * export is the in-repo SSOT; keep package.json in lockstep. + * `package.json` → `interchange.grantRequirements` is the single source, so a + * host installer can read npm metadata without executing code; this module + * loads and validates it for in-process callers. * * Shape matches Interchange definition grant requirements * (`resource` + `action` + `installHint`). Control plane materializes grants * onto the workflow principal at deploy/launch. */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { type } from "arktype"; -/** - * Advisory-only sizing hint for install tooling deciding how broadly to mint - * the underlying `resource`/`action` capability grant (e.g. "give every - * tenant member `memory:search`" vs "give this principal `memory:forget` - * scoped to what it creates"). **Nothing in this package reads or enforces - * this value** — it is not a `requireGrant`/`canAccessDocument` mode switch, - * and it does not gate anything at request time. Whether a specific caller - * may actually forget/purge a specific document is decided entirely by the - * imperative creator check in `services/retention-ownership.ts` (wired into - * `memory.ts`), independent of grant tags and of this field. See - * ARCHITECTURE.md § Boundaries for the two-mechanism split. - */ -export type MemoryGrantInstallHint = "tenant" | "creator" | "invoker"; +const GrantRequirement = type({ + resource: "string", + action: "string", + /** + * Advisory-only sizing hint for install tooling deciding how broadly to + * mint the underlying `resource`/`action` capability grant (e.g. "give + * every tenant member `memory:search`" vs "give this principal + * `memory:forget` scoped to what it creates"). **Nothing in this package + * reads or enforces this value** — whether a specific caller may actually + * forget/purge a specific document is decided entirely by the imperative + * creator check in `services/retention-ownership.ts`. See ARCHITECTURE.md + * § Boundaries for the two-mechanism split. + */ + installHint: "'tenant' | 'creator' | 'invoker'", + /** Package surfaces that need the requirement when installed. */ + surfaces: "('tools' | 'distiller' | 'routes')[]", +}); -/** Package surfaces that need the requirement when installed. */ -export type MemoryGrantSurface = "tools" | "distiller" | "routes"; +export type MemoryGrantRequirement = typeof GrantRequirement.infer; +export type MemoryGrantInstallHint = MemoryGrantRequirement["installHint"]; +export type MemoryGrantSurface = MemoryGrantRequirement["surfaces"][number]; -export type MemoryGrantRequirement = { - readonly resource: string; - readonly action: string; - /** Install-sizing hint only — see `MemoryGrantInstallHint`. Not enforced. */ - readonly installHint: MemoryGrantInstallHint; - readonly surfaces: readonly MemoryGrantSurface[]; -}; +const PackageGrantRequirements = type({ + interchange: { grantRequirements: GrantRequirement.array() }, +}); /** * Minimum capability grants for memory tools / routes / process helpers. * Document-tag access (`memory.doc:…`, `memory.space:…`) is separate and * minted per document — not package install requirements. + * + * `forget` and `purge` are `installHint: "creator"`: a sizing suggestion + * only. The per-document ownership check runs in + * `services/retention-ownership.ts` regardless of how broadly they are minted. */ -export const MEMORY_GRANT_REQUIREMENTS = [ - { - resource: "memory", - action: "add", - installHint: "tenant", - surfaces: ["tools", "distiller", "routes"], - }, - { - resource: "memory", - action: "search", - installHint: "tenant", - surfaces: ["tools", "distiller", "routes"], - }, - /** - * Retention writes (CL-6288). Tombstone and retention-class changes share - * `forget`; hard delete gets its own `purge` so a host can hand out "let - * this user forget their own notes" without also handing out irreversible - * deletion. `installHint: "creator"` is a sizing suggestion for install - * tooling ONLY — the real per-document ownership check that stops a - * caller from forgetting/purging someone else's document runs in - * `services/retention-ownership.ts` regardless of how broadly this grant - * was minted. - */ - { - resource: "memory", - action: "forget", - installHint: "creator", - surfaces: ["routes"], - }, - { - resource: "memory", - action: "purge", - installHint: "creator", - surfaces: ["routes"], - }, -] as const satisfies readonly MemoryGrantRequirement[]; +export const MEMORY_GRANT_REQUIREMENTS: readonly MemoryGrantRequirement[] = + PackageGrantRequirements.assert( + JSON.parse( + // dirname (not Bun-only `dir`): package.json sits one level above both + // src/ and the packed dist/. + readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8"), + ), + ).interchange.grantRequirements; /** Compact `resource:action` form used on agent `capabilities` arrays. */ export const MEMORY_CAPABILITY_IDS = MEMORY_GRANT_REQUIREMENTS.map( - (r) => `${r.resource}:${r.action}` as const, + (r) => `${r.resource}:${r.action}`, ); /** @@ -91,6 +71,6 @@ export function capabilityIdsForSurface( surface: MemoryGrantSurface, ): string[] { return MEMORY_GRANT_REQUIREMENTS.filter((r) => - (r.surfaces as readonly MemoryGrantSurface[]).includes(surface), + r.surfaces.includes(surface), ).map((r) => `${r.resource}:${r.action}`); }