diff --git a/AGENTS.md b/AGENTS.md index 0b05bd8..fb30a06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,8 +24,11 @@ CI runs `typecheck` + `test` — both must pass before any push. - `src/mount-config.ts` / `src/config.ts` — mount config + engine config - `src/routes/` — Hono routes (`add`, `search`, `list`, `feed`, retention `forget`/`purge`/`retention-class`) -- `src/tools/` — Interchange `defineTool` factories (`@corbits/memory/tools`); - HTTP clients for mounted routes (env credentials; no in-process plane) +- `src/workflow-mount.ts` — run-scoped routes for deployed agents (agent-token + bearer + run address; no session auth) +- `src/sidecar-bundle.ts` + `src/tools.ts` — the agent tool bundle + (`@corbits/memory/sidecar-bundle`) and its tool descriptors +- `src/http-client.ts` — host-side HTTP client for the tenant routes - `src/services/` — capture / search / transform internals (not public verbs) - `src/ports/` — `DocumentStore` / `SourceProvider` + fakes - `src/core/` — embed/rerank clients, merge, arktype schemas diff --git a/README.md b/README.md index 55b9aaa..2faa74d 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ Memory for [Interchange](https://github.com/corbitsdev) hubs: **add**, **search* Mount it on the hub. Routes land under `/api/tenants/:tenantId/memory/*`, so the hub’s existing `createResolveTenant` middleware supplies principal + tenant -— same as workflows, assets, and agents. Workflow agents install the package’s -`defineTool` factories; ingestion modules call the same routes or the in-process -plane. That’s the product. +— same as workflows, assets, and agents. Deployed agents carry the sidecar +bundle and call the run-scoped routes under `/api/workflow-memory/*`; +ingestion modules call the tenant routes or the in-process plane. That’s the +product. Requires Bun 1.2+. @@ -57,46 +58,47 @@ POST /api/tenants/:tenantId/memory/search { "query", "limit"?, "kinds"?, "enti GET /api/tenants/:tenantId/memory/list ?limit= ``` -## Workflow agent tools +## Agent tools (sidecar bundle) -This package exports Interchange `defineTool` factories at -`@corbits/memory/tools` (also `package.json` → `interchange.tools`). Each tool -is a thin HTTP client: install credentials in agent env, call the mounted hub -routes. No plane inject, no model-supplied identity. +A deployed agent carries the memory tools through one factory at +`@corbits/memory/sidecar-bundle`. It holds no client code, no base URL and no +token: it resolves the `hub` credential handle from the host-assembled runtime +capabilities and calls the run-scoped routes through that mediated fetch, +naming its run with the `x-workflow-run-address` header. -| Factory id | Tool name | HTTP | -| --- | --- | --- | -| `@corbits/memory/add` | `memory_add` | `POST …/memory/add` | -| `@corbits/memory/search` | `memory_search` | `POST …/memory/search` | -| `@corbits/memory/list` | `memory_list` | `GET …/memory/list` | - -**Env keys** (declared on each factory’s `requires`): - -| Key | Meaning | +| Tool | Route | | --- | --- | -| `memoryBaseUrl` | Hub **origin** only, e.g. `https://hub.example` (no `/api/...` path) | -| `memoryTenantId` | Tenant path segment (must match the principal’s tenant on the hub) | -| `memoryAuthToken` | Bearer token the hub accepts for that agent principal | - -**Host checklist** +| `memory_add` | `POST /api/workflow-memory/add` | +| `memory_search` | `POST /api/workflow-memory/search` | +| `memory_list` | `GET /api/workflow-memory/list` | +| `memory_feed` | `GET /api/workflow-memory/feed` | -1. Mount routes: `createMemory({ app, grantStore, … })` under the hub tenant tree. -2. Grant the agent principal `memory:add` and/or `memory:search` (`list` uses `search`). -3. For peer/space share visibility, also grant `search` on the relevant document tags (see `docs/AUTHZ-DOCUMENT-ACCESS.md`). -4. Install factories on the workflow and set the three env keys above. -5. Auth is **Bearer only** on the tool client — session cookies are not sent. -6. Tool results are **JSON strings** (`stringTool`); pass `AbortSignal` if you need hang protection (no default client timeout). +Those routes are a **second, parallel mount** — the tenant routes above keep +their session auth untouched: ```ts -import { memoryAdd, memorySearch, memoryList } from "@corbits/memory/tools"; +import { mountWorkflowMemory } from "@corbits/memory"; -// On a workflow / agent definition — install like any open tool package: -// tools: [memoryAdd, memorySearch, memoryList] -// and supply memoryBaseUrl / memoryTenantId / memoryAuthToken in agent env. +mountWorkflowMemory(workflowMemoryApi, { + memory, + agentToken: { verify, resolveRun }, // host's own token + run lookup +}); +app.route("/api/workflow-memory", workflowMemoryApi); ``` -OpenAPI→MCP remains available as an alternative host bridge; the shipped -`defineTool`s are the primary install path for workflow agents. +`verify` returns `{ tenantId, definitionId }` for a recognized bearer (or +`undefined`), `resolveRun` maps the run address to `{ tenantId, principalId, +runId }`. A token whose tenant is not the run's tenant is refused with the same +401 as an unknown bearer. Every call is scoped to that run's tenant and +principal; the body never carries identity. + +**Host checklist** + +1. Mount the tenant routes: `createMemory({ app, grantStore, … })`. +2. Mount `mountWorkflowMemory` and bind the agent's hub credential to the + `hub` handle on its definition. +3. For peer/space share visibility, grant `search` on the relevant document + tags (see `docs/AUTHZ-DOCUMENT-ACCESS.md`). ## Ingestion (in-process) diff --git a/bun.lock b/bun.lock index a2bdc12..ef51615 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", + "@intx/types": "^0.3.0", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", @@ -45,64 +46,6 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@hono/standard-validator": ["@hono/standard-validator@0.2.3", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "hono": ">=3.9.0" } }, "sha512-bp9vHu6Va6SfMHC3D4ZLBbT/woi+AZ9CRdTXQu3kLJuLh2W/Gb9UO4hijS+BQAGFXi4EGpXdetxpzwTAawSVeg=="], @@ -133,7 +76,7 @@ "@intx/tool-packaging": ["@intx/tool-packaging@0.2.2", "", { "dependencies": { "@intx/agent": "0.2.2", "@intx/log": "0.2.2", "@intx/types": "0.2.2", "arktype": "^2.1.29", "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-registry-fetch": "^19.0.0", "semver": "^7.7.2", "ssri": "^12.0.0", "tar": "^7.5.1" } }, "sha512-luyHVkZSLMN/bWfTD2nLF7l6h7w/NQfFhNX1vfNhl/8DsG0lUFEn5NCjK/07eyF6iyVDMDGl3Cuq9r7B2y7deA=="], - "@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], + "@intx/types": ["@intx/types@0.3.0", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-PJ+v3IhtfZ4J7ZqJ8E39TteRMnC6Y503Q0JeCJdskkK+jhcjx01z7TJSozucagauAlpyh8Hn/9CyWFl53Ji0DQ=="], "@intx/workflow": ["@intx/workflow@0.2.2", "", { "dependencies": { "@intx/agent": "0.2.2", "@intx/inference": "0.2.2", "arktype": "^2.1.29" } }, "sha512-/vx8UDtPFjY0K0KoIaHmojg3up0/iOCF9CzTIludHVFWUF/Pc224ZxkqMkL5ncwgUbBQpoIqAAUAmuYMp8JT5Q=="], @@ -155,8 +98,6 @@ "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], @@ -195,8 +136,6 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], @@ -223,8 +162,6 @@ "diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="], - "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -235,8 +172,6 @@ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], @@ -245,16 +180,12 @@ "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -371,8 +302,6 @@ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -397,10 +326,6 @@ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -409,8 +334,6 @@ "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], - "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -427,75 +350,51 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@intx/agent/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@intx/authz/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + "@intx/crypto/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "cacache/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + "@intx/db/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "cacache/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + "@intx/hub-api/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "make-fetch-happen/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + "@intx/hub-common/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "make-fetch-happen/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + "@intx/hub-sessions/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@intx/inference/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@intx/mime/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "npm-registry-fetch/npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="], + "@intx/pack-transport/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "npm-registry-fetch/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + "@intx/storage-isogit/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + "@intx/tool-packaging/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "@intx/workflow-deploy/@intx/types": ["@intx/types@0.2.2", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-9DYTXLuf6ARsHWOWnRuOaA/+5haM3yJVUhvzlps/eQTH6zjk3CMOlbgKUtBYZ164MhC5IbXCvJTLGyM1grpMqw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + "cacache/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + "cacache/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + "make-fetch-happen/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + "make-fetch-happen/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + "npm-registry-fetch/npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + "npm-registry-fetch/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -505,58 +404,6 @@ "npm-registry-fetch/npm-package-arg/validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - "npm-registry-fetch/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], } } diff --git a/docs/DISTILLER.md b/docs/DISTILLER.md index 66f4138..0ac50e4 100644 --- a/docs/DISTILLER.md +++ b/docs/DISTILLER.md @@ -38,7 +38,7 @@ When other writers also `add`, drain new versions with the capture feed: ```ts import { runDistillTick } from "@corbits/memory/distiller"; -import { createMemoryHttpClient } from "@corbits/memory/tools"; +import { createMemoryHttpClient } from "@corbits/memory"; const client = createMemoryHttpClient({ baseUrl: process.env.MEMORY_BASE_URL!, @@ -92,7 +92,7 @@ const { workflow, generatorAgentId } = createResidentDistiller({ | Claim identity on add | `generator_agent_id`, `provenance`, `lineage_class`, `derived_from` | | Wire attribution on search | `SearchItem.attribution` | | Retention / forgetting | [RETENTION.md](./RETENTION.md) | -| Tools | `@corbits/memory/tools` | +| Tools | `@corbits/memory/sidecar-bundle` | ## Grant manifest (process principal) @@ -100,7 +100,7 @@ Installer discovery (not live grants): - `package.json` → `interchange.grantRequirements` - typed SSOT: `MEMORY_GRANT_REQUIREMENTS` / `MEMORY_CAPABILITY_IDS` from - `@corbits/memory` (or `@corbits/memory/tools`) + `@corbits/memory` Minimum capabilities: diff --git a/package.json b/package.json index 3168edc..bd3437c 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,10 @@ ".": "./src/index.ts", "./migrations": "./src/migrations.ts", "./config": "./src/mount-config.ts", - "./tools": "./src/tools/index.ts", + "./sidecar-bundle": "./src/sidecar-bundle.ts", "./distiller": "./src/distiller/index.ts" }, "interchange": { - "tools": "./src/tools/index.ts", "grantRequirements": [ { "resource": "memory", @@ -55,6 +54,7 @@ "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", + "@intx/types": "^0.3.0", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", diff --git a/src/distiller/tick.test.ts b/src/distiller/tick.test.ts index c85383c..fde5044 100644 --- a/src/distiller/tick.test.ts +++ b/src/distiller/tick.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; -import type { MemoryHttpClient } from "../tools/client.ts"; +import type { MemoryHttpClient } from "../http-client.ts"; import { RESIDENT_DISTILLER_AGENT_ID } from "./constants.ts"; import { runDistillTick } from "./tick.ts"; diff --git a/src/distiller/tick.ts b/src/distiller/tick.ts index aca4c62..afcab30 100644 --- a/src/distiller/tick.ts +++ b/src/distiller/tick.ts @@ -2,7 +2,7 @@ * Imperative distill tick — for hosts that want a function, not a workflow. * Inference stays injected: host supplies `distill` (call your model). */ -import type { MemoryHttpClient } from "../tools/client.ts"; +import type { MemoryHttpClient } from "../http-client.ts"; import { buildDistilledClaim, shouldProcessFeedEntry, diff --git a/src/distiller/workflow.test.ts b/src/distiller/workflow.test.ts index f7f1956..7c52fb4 100644 --- a/src/distiller/workflow.test.ts +++ b/src/distiller/workflow.test.ts @@ -4,6 +4,7 @@ import { RESIDENT_DISTILLER_AGENT_ID, RESIDENT_DISTILLER_WORKFLOW_ID, } from "./constants.ts"; +import { SIDECAR_BUNDLE_ID } from "../sidecar-bundle.ts"; import { createResidentDistiller } from "./workflow.ts"; describe("createResidentDistiller", () => { @@ -20,7 +21,10 @@ describe("createResidentDistiller", () => { { type: "schedule", cron: "*/5 * * * *" }, ]); expect(agent.id).toBe(RESIDENT_DISTILLER_AGENT_ID); - expect(agent.toolFactories.length).toBeGreaterThanOrEqual(3); + // One factory: the memory sidecar bundle carries every memory tool. + expect(agent.toolFactories.map((factory) => factory.id)).toEqual([ + SIDECAR_BUNDLE_ID, + ]); expect(agent.systemPrompt).toContain("memory_feed"); expect(agent.systemPrompt).toContain(RESIDENT_DISTILLER_AGENT_ID); }); diff --git a/src/distiller/workflow.ts b/src/distiller/workflow.ts index 34d96b1..f649f5e 100644 --- a/src/distiller/workflow.ts +++ b/src/distiller/workflow.ts @@ -3,12 +3,12 @@ * * ```ts * import { createResidentDistiller } from "@corbits/memory/distiller"; - * import { memoryAdd, memoryFeed, memorySearch } from "@corbits/memory/tools"; * * const workflow = createResidentDistiller({ * inference: { sources: [{ provider: "openai", model: "gpt-4.1-mini" }] }, * }); - * // deploy with host workflow-deploy + env: memoryBaseUrl, memoryTenantId, memoryAuthToken + * // deploy with host workflow-deploy; the host binds the `hub` credential + * // handle the sidecar bundle resolves at run time * ``` */ import { @@ -20,9 +20,7 @@ import { } from "@intx/agent"; import { defineWorkflow, type WorkflowDefinition } from "@intx/workflow"; -import { memoryAdd } from "../tools/add.ts"; -import { memoryFeed } from "../tools/feed.ts"; -import { memorySearch } from "../tools/search.ts"; +import { memory } from "../sidecar-bundle.ts"; import { capabilityIdsForSurface } from "../grant-requirements.ts"; import { RESIDENT_DISTILLER_AGENT_ID, @@ -61,8 +59,8 @@ export type CreateResidentDistillerOpts = { /** Override system prompt. */ systemPrompt?: string; /** - * Extra tool factories beyond memory_feed / memory_add / memory_search. - * Default tools are always included first. + * Extra tool factories beyond the memory sidecar bundle, which is always + * included first. */ extraTools?: readonly AnnotatedToolFactory[]; /** Optional agent description. */ @@ -84,11 +82,9 @@ export function createResidentDistiller( ): ResidentDistiller { const generatorAgentId = opts.agentId ?? RESIDENT_DISTILLER_AGENT_ID; const tools = [ - memoryFeed, - memoryAdd, - memorySearch, + memory, ...(opts.extraTools ?? []), - ] as AnnotatedToolFactory[]; + ] as unknown as AnnotatedToolFactory[]; const agent = defineAgent({ id: generatorAgentId, diff --git a/src/tools/client.ts b/src/http-client.ts similarity index 71% rename from src/tools/client.ts rename to src/http-client.ts index 02b44e1..aa44e44 100644 --- a/src/tools/client.ts +++ b/src/http-client.ts @@ -1,13 +1,13 @@ /** - * Thin HTTP client for mounted hub memory routes. + * Thin HTTP client a HOST uses to call the tenant memory routes + * (`/api/tenants/:tenantId/memory/*`) with its own credentials — the + * imperative distill tick takes one. Agents never use it: they carry the + * sidecar bundle, which calls the run-scoped routes instead. * - * Tools never touch the in-process plane — they only call - * `/api/tenants/:tenantId/memory/*` with credentials from install env. - * - * Pass `signal` on each call (or via the tool runner) so a hung hub can be - * cancelled; this client does not invent a default timeout. + * Pass `signal` on each call so a hung hub can be cancelled; this client + * does not invent a default timeout. */ -import type { AddRequest, SearchRequest } from "../http-bodies.ts"; +import type { AddRequest, SearchRequest } from "./http-bodies.ts"; export type MemoryHttpConfig = { baseUrl: string; @@ -155,42 +155,3 @@ export function createMemoryHttpClient( }; } -/** Env keys declared by every memory tool factory via `requires`. */ -export const MEMORY_TOOL_ENV_KEYS = [ - "memoryBaseUrl", - "memoryTenantId", - "memoryAuthToken", -] as const; - -export type MemoryToolEnvKeys = (typeof MEMORY_TOOL_ENV_KEYS)[number]; - -export type MemoryToolEnv = { - memoryBaseUrl: string; - memoryTenantId: string; - memoryAuthToken: string; - /** - * Optional host/test inject. Not part of `requires` — agents never set this. - */ - memoryFetch?: typeof globalThis.fetch; -}; - -export function readMemoryToolEnv(env: MemoryToolEnv): MemoryHttpConfig { - const baseUrl = env.memoryBaseUrl; - const tenantId = env.memoryTenantId; - const authToken = env.memoryAuthToken; - if (typeof baseUrl !== "string" || baseUrl.length === 0) { - throw new Error("memoryBaseUrl must be a non-empty string"); - } - if (typeof tenantId !== "string" || tenantId.length === 0) { - throw new Error("memoryTenantId must be a non-empty string"); - } - if (typeof authToken !== "string" || authToken.length === 0) { - throw new Error("memoryAuthToken must be a non-empty string"); - } - return { - baseUrl, - tenantId, - authToken, - ...(env.memoryFetch !== undefined ? { fetch: env.memoryFetch } : {}), - }; -} diff --git a/src/index.ts b/src/index.ts index 1b0a848..1b77c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -195,6 +195,27 @@ export { } from "./core/embed-model-registry.ts"; +// Run-scoped routes for deployed agents (bearer + run address, no session). +// The tools that call them ship at `@corbits/memory/sidecar-bundle`. +export { mountWorkflowMemory } from "./workflow-mount.ts"; +export type { + AgentTokenAuth, + AgentTokenIdentity, + MountWorkflowMemoryOpts, + ResolvedWorkflowRunScope, + WorkflowMemoryEnv, +} from "./workflow-mount.ts"; +export { MEMORY_TOOL_DEFINITIONS, type MemoryToolDefinition } from "./tools.ts"; + +// Host-side HTTP client for the tenant routes (the imperative distill tick). +export { + createMemoryHttpClient, + type MemoryAddBody, + type MemoryHttpClient, + type MemoryHttpConfig, + type MemorySearchBody, +} from "./http-client.ts"; + // Migrations export { runMemoryMigrations } from "./migrations.ts"; diff --git a/src/sidecar-bundle.test.ts b/src/sidecar-bundle.test.ts new file mode 100644 index 0000000..05637c7 --- /dev/null +++ b/src/sidecar-bundle.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, test } from "bun:test"; + +import { MEMORY_TOOL_DEFINITIONS } from "./tools.ts"; +import { HUB_CREDENTIAL_HANDLE, memory, SIDECAR_BUNDLE_ID } from "./sidecar-bundle.ts"; + +type Recorded = { url: string; init?: RequestInit }; + +function env(recorded: Recorded[], respond: () => Response) { + const credential = { + kind: "http" as const, + fetch: (input: string | URL | Request, init?: RequestInit) => { + recorded.push({ url: String(input), ...(init !== undefined ? { init } : {}) }); + return Promise.resolve(respond()); + }, + dispose: () => undefined, + }; + return { + address: "run-1@acme.example.com", + capabilities: { + resolve: (key: string) => { + if (key !== "credentials") throw new Error(`unexpected capability ${key}`); + return { + resolve: (handle: string) => { + if (handle !== HUB_CREDENTIAL_HANDLE) { + throw new Error(`unexpected handle ${handle}`); + } + return Promise.resolve(credential); + }, + }; + }, + }, + } as never; +} + +const ok = () => + new Response(JSON.stringify({ data: { documentId: "doc_1", versionId: "ver_1" } }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + +const signal = new AbortController().signal; + +describe("the memory sidecar bundle", () => { + test("declares every memory tool under the package-namespaced id", () => { + expect(memory.id).toBe(SIDECAR_BUNDLE_ID); + expect(memory.requires).toEqual(["capabilities", "address"]); + const bundle = memory(env([], ok)); + expect(bundle.definitions.map((def) => def.name)).toEqual( + MEMORY_TOOL_DEFINITIONS.map((def) => def.name), + ); + }); + + test("adds through the run-scoped route, carrying the run address", async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + const result = await bundle.run( + { + id: "call-1", + name: "memory_add", + arguments: { title: "Notes", text: "body", kind: "note" }, + }, + signal, + ); + + expect(result.isError).toBeUndefined(); + expect(recorded[0]?.url).toBe("/api/workflow-memory/add"); + const headers = recorded[0]?.init?.headers as Record; + expect(headers["x-workflow-run-address"]).toBe("run-1@acme.example.com"); + expect(JSON.parse(String(recorded[0]?.init?.body))).toEqual({ + title: "Notes", + text: "body", + kind: "note", + }); + }); + + test("resolves the credential once across calls", async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + await bundle.run({ id: "a", name: "memory_list", arguments: {} }, signal); + await bundle.run({ id: "b", name: "memory_list", arguments: {} }, signal); + expect(recorded).toHaveLength(2); + await bundle.dispose?.(); + }); + + test("a hub error comes back as a tool error, never a throw", async () => { + const bundle = memory( + env( + [], + () => + new Response(JSON.stringify({ error: "search failed" }), { + status: 502, + headers: { "content-type": "application/json" }, + }), + ), + ); + const result = await bundle.run( + { id: "c", name: "memory_search", arguments: { query: "q" } }, + signal, + ); + expect(result.isError).toBe(true); + expect(result.content).toBe("search failed"); + }); + + test("an unknown tool name is a tool error", async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + const result = await bundle.run({ id: "d", name: "memory_nope", arguments: {} }, signal); + expect(result.isError).toBe(true); + expect(recorded).toHaveLength(0); + }); +}); + +describe("every declared tool maps onto a route", () => { + const calls: Array<[string, Record, string]> = [ + ["memory_add", { title: "T", text: "b" }, "/api/workflow-memory/add"], + ["memory_search", { query: "q", limit: 5 }, "/api/workflow-memory/search"], + ["memory_list", { limit: 5 }, "/api/workflow-memory/list?limit=5"], + [ + "memory_feed", + { after: 3, limit: 10, exclude_generator: "resident-distiller" }, + "/api/workflow-memory/feed?after=3&limit=10&exclude_generator=resident-distiller", + ], + ]; + + for (const [name, args, url] of calls) { + test(`${name} calls ${url}`, async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + const result = await bundle.run({ id: name, name, arguments: args }, signal); + expect(result.isError).toBeUndefined(); + expect(recorded[0]?.url).toBe(url); + await bundle.dispose?.(); + }); + } + + test("the search body forwards only schema fields", async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + await bundle.run( + { + id: "s", + name: "memory_search", + arguments: { query: "q", kinds: ["note"], tenantId: "spoofed" }, + }, + signal, + ); + expect(JSON.parse(String(recorded[0]?.init?.body))).toEqual({ + query: "q", + kinds: ["note"], + }); + }); + + test("the loader's namespaced name resolves to the declared one", async () => { + const recorded: Recorded[] = []; + const bundle = memory(env(recorded, ok)); + await bundle.run( + { id: "n", name: "@corbits/memory/sidecar-bundle:memory_list", arguments: {} }, + signal, + ); + expect(recorded[0]?.url).toBe("/api/workflow-memory/list"); + }); + + test("a body-less hub error still reads as a tool error", async () => { + const bundle = memory(env([], () => new Response("nope", { status: 500 }))); + const result = await bundle.run({ id: "e", name: "memory_list", arguments: {} }, signal); + expect(result.isError).toBe(true); + expect(result.content).toBe("the hub answered 500"); + }); +}); diff --git a/src/sidecar-bundle.ts b/src/sidecar-bundle.ts new file mode 100644 index 0000000..f073a71 --- /dev/null +++ b/src/sidecar-bundle.ts @@ -0,0 +1,236 @@ +// Sidecar-bundle entry for `@corbits/memory` — the convention-compliant +// factory the tool-package loader invokes, so a deployed agent carries the +// memory tools without any agent-owned client code. +// +// The bundle holds no database handle and no secret. It resolves the `hub` +// credential handle from the host-assembled runtime capabilities and calls +// the run-scoped memory routes through that mediated fetch, which is pinned +// to the hub's own origin and injects the agent's bearer per request. The +// env keys it touches (`capabilities`, `address`) are declared in `requires`: +// `address` is the run address the routes scope every call to. +import { defineTool, type BaseEnv } from "@intx/agent"; +import type { ToolCall, ToolResult } from "@intx/types/runtime"; + +import { MEMORY_TOOL_DEFINITIONS } from "./tools.ts"; + +/** Where a host mounts `mountWorkflowMemory`. The bundle has no options of + * its own — the loader constructs it — so the path is a shared constant + * rather than per-deploy configuration. */ +export const WORKFLOW_MEMORY_BASE_PATH = "/api/workflow-memory"; + +/** The credential handle this package declares, and the one a host binds the + * agent's hub token to. */ +export const HUB_CREDENTIAL_HANDLE = "hub"; + +export const SIDECAR_BUNDLE_ID = "@corbits/memory/sidecar-bundle"; + +type MediatedFetch = ( + input: string | URL | Request, + init?: RequestInit, +) => Promise; + +type HttpCredential = { + readonly kind: string; + fetch: MediatedFetch; + dispose(): void | Promise; +}; + +/** The slice of the host-assembled runtime capabilities this bundle uses. */ +type CredentialCapabilities = { + resolve(key: "credentials"): { resolve(handle: string): Promise }; +}; + +/** The env keys `requires` declares, on top of the core ones. */ +export type MemoryToolEnv = BaseEnv & { + readonly capabilities: CredentialCapabilities; + readonly address: string; +}; + +type Request_ = { + readonly method: "GET" | "POST"; + readonly path: string; + readonly body?: Record; +}; + +function query(params: Record): string { + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null || value === "") continue; + search.set(key, String(value)); + } + const serialized = search.toString(); + return serialized === "" ? "" : `?${serialized}`; +} + +function defined( + entries: Record, +): Record { + return Object.fromEntries( + Object.entries(entries).filter(([, value]) => value !== undefined), + ); +} + +/** Maps one model-facing tool call onto the run-scoped route that performs + * it. An unknown name returns undefined and answers as a tool error. */ +function requestFor(name: string, args: Record): Request_ | undefined { + switch (name) { + case "memory_add": + return { + method: "POST", + path: "/add", + body: defined({ + title: args["title"], + text: args["text"], + access_tags: args["access_tags"], + kind: args["kind"], + generator_agent_id: args["generator_agent_id"], + provenance: args["provenance"], + lineage_class: args["lineage_class"], + temporal_class: args["temporal_class"], + derived_from: args["derived_from"], + valid_from: args["valid_from"], + valid_until: args["valid_until"], + share: args["share"], + }), + }; + case "memory_search": + return { + method: "POST", + path: "/search", + body: defined({ + query: args["query"], + limit: args["limit"], + kinds: args["kinds"], + entity_ids: args["entity_ids"], + sources: args["sources"], + includeEvidence: args["includeEvidence"], + includeDeprecated: args["includeDeprecated"], + }), + }; + case "memory_list": + return { method: "GET", path: `/list${query({ limit: args["limit"] })}` }; + case "memory_feed": + return { + method: "GET", + path: `/feed${query({ + after: args["after"], + limit: args["limit"], + exclude_generator: args["exclude_generator"], + })}`, + }; + default: + return undefined; + } +} + +/** The loader may present a namespaced name; the switch above keys on the + * declared one. */ +function bareToolName(name: string): string { + const colon = name.lastIndexOf(":"); + return colon === -1 ? name : name.slice(colon + 1); +} + +async function readErrorMessage(response: Response): Promise { + const body: unknown = await response.json().catch(() => undefined); + if (typeof body === "object" && body !== null && "error" in body) { + const error = (body as { error: unknown }).error; + if (typeof error === "string") return error; + if (typeof error === "object" && error !== null && "message" in error) { + const message = (error as { message: unknown }).message; + if (typeof message === "string") return message; + } + } + return `the hub answered ${String(response.status)}`; +} + +/** The path is relative on purpose: a mediated http handle resolves it + * against the origin it is pinned to, so the bundle never names a host. */ +export async function callMemoryRoute( + fetchImpl: MediatedFetch, + runAddress: string, + request: Request_, +): Promise { + const response = await fetchImpl(`${WORKFLOW_MEMORY_BASE_PATH}${request.path}`, { + method: request.method, + headers: { + "x-workflow-run-address": runAddress, + ...(request.body !== undefined ? { "content-type": "application/json" } : {}), + }, + ...(request.body !== undefined ? { body: JSON.stringify(request.body) } : {}), + }); + if (!response.ok) throw new Error(await readErrorMessage(response)); + const payload: unknown = await response.json().catch(() => undefined); + if (typeof payload === "object" && payload !== null && "data" in payload) { + return (payload as { data: unknown }).data; + } + return payload; +} + +// Bound to a const rather than passed as a fresh object literal: some +// `@intx/agent` versions add a static `definitions` field to defineTool's +// options, which an older pinned version predates — structural assignment +// type-checks against either shape. +const bundleOpts = { + id: SIDECAR_BUNDLE_ID, + requires: ["capabilities", "address"] as const, + definitions: MEMORY_TOOL_DEFINITIONS.map((def) => ({ name: def.name })), + factory: (env: MemoryToolEnv) => { + let handle: Promise | undefined; + + function hub(): Promise { + handle ??= (async () => { + const credential = await env.capabilities + .resolve("credentials") + .resolve(HUB_CREDENTIAL_HANDLE); + if (credential.kind !== "http") { + throw new Error( + `the "${HUB_CREDENTIAL_HANDLE}" credential is a ${credential.kind} handle; the memory tools need an http one`, + ); + } + return credential; + })(); + return handle; + } + + return { + definitions: MEMORY_TOOL_DEFINITIONS.map((def) => ({ + name: def.name, + description: def.description, + inputSchema: def.inputSchema as unknown as Record, + })), + async run(call: ToolCall): Promise { + const name = bareToolName(call.name); + const request = requestFor(name, call.arguments); + if (request === undefined) { + return { callId: call.id, content: `unknown memory tool: ${call.name}`, isError: true }; + } + try { + const credential = await hub(); + const data = await callMemoryRoute( + (input, init) => credential.fetch(input, init), + env.address, + request, + ); + return { callId: call.id, content: JSON.stringify(data) }; + } catch (error) { + return { + callId: call.id, + content: error instanceof Error ? error.message : String(error), + isError: true, + }; + } + }, + async dispose() { + if (handle === undefined) return; + await (await handle).dispose(); + }, + }; + }, +}; + +/** + * The named export the loader picks up. `factory` is synchronous while the + * credential resolve is not, so the handle is resolved lazily on first use + * and the promise memoized — one resolve, one handle to dispose. + */ +export const memory = defineTool(bundleOpts); diff --git a/src/tools.ts b/src/tools.ts new file mode 100644 index 0000000..3b32f2b --- /dev/null +++ b/src/tools.ts @@ -0,0 +1,174 @@ +/** + * Tool descriptors for hosts that register memory behaviors with an agent + * runtime. Structural, not imported from a runtime package: this module + * stays free of any agent-SDK dependency, and `sidecar-bundle.ts` binds each + * name to the run-scoped route that performs it. + */ +import { LIST_LIMIT_MAX, LIST_LIMIT_MIN, SEARCH_LIMIT_MAX, SEARCH_LIMIT_MIN } from "./limits.ts"; + +export type MemoryToolDefinition = { + name: string; + description: string; + inputSchema: { + type: "object"; + properties: Record; + required?: string[]; + additionalProperties: false; + }; +}; + +export const MEMORY_TOOL_DEFINITIONS: readonly MemoryToolDefinition[] = [ + { + name: "memory_add", + description: + "Store a note in tenant memory. Returns { documentId, versionId }. " + + "For distilled claims set generator_agent_id, provenance=inferred, " + + "lineage_class=derived, and derived_from source version ids. " + + "Identity is the run this agent is acting for.", + inputSchema: { + type: "object", + properties: { + title: { type: "string", description: "Short title for the document" }, + text: { type: "string", description: "Full body text to store" }, + access_tags: { + type: "array", + items: { type: "string" }, + description: + "Optional grant-pattern tags controlling document visibility " + + "(distiller: copy from source feed entry, never widen)", + }, + kind: { type: "string", description: "Document kind (default note)" }, + generator_agent_id: { + type: "string", + description: + "Agent id that authored this version (e.g. resident-distiller). " + + "Enables feed exclude_generator loop-safety.", + }, + provenance: { + type: "string", + enum: ["stated", "inferred", "unknown"], + description: "How content was obtained (inferred for distilled claims)", + }, + lineage_class: { + type: "string", + enum: ["native", "imported", "derived"], + description: "Data lineage (derived for distilled claims)", + }, + temporal_class: { + type: "string", + enum: ["event", "deadline", "state", "lesson"], + description: "Temporal ranking class", + }, + derived_from: { + type: "array", + items: { type: "string" }, + description: "Source version ids this claim is derived from", + }, + valid_from: { type: "string", description: "Optional validity start (ISO)" }, + valid_until: { type: "string", description: "Optional validity end (ISO)" }, + share: { + type: "object", + properties: { + tenant: { type: "boolean" }, + principals: { type: "array", items: { type: "string" } }, + tags: { type: "array", items: { type: "string" } }, + }, + description: + "Optional share sugar that mints access tags (tenant / principals / tags)", + }, + }, + required: ["title", "text"], + additionalProperties: false, + }, + }, + { + name: "memory_search", + description: + "Hybrid semantic + keyword search over tenant memory. " + + "Returns ranked items with optional additive attribution " + + "(provenance, temporal class, corroboration, derivedFrom) and evidence. " + + "Attribute stated content to the actor; treat inferred as own-voice claims.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query text" }, + limit: { + type: "integer", + minimum: SEARCH_LIMIT_MIN, + maximum: SEARCH_LIMIT_MAX, + description: `Max hits to return (${SEARCH_LIMIT_MIN}–${SEARCH_LIMIT_MAX})`, + }, + kinds: { + type: "array", + items: { type: "string" }, + description: "Optional document kind filter", + }, + entity_ids: { + type: "array", + items: { type: "string" }, + description: "Optional entity-id filter", + }, + sources: { + type: "array", + items: { type: "string" }, + description: 'Optional channel filter (e.g. "local" and/or live source ids)', + }, + includeEvidence: { + type: "boolean", + description: "Include evidence strength on the response", + }, + includeDeprecated: { + type: "boolean", + description: "Include deprecated versions in results (default false)", + }, + }, + required: ["query"], + additionalProperties: false, + }, + }, + { + name: "memory_list", + description: "List recent documents visible to this run in the tenant's memory.", + inputSchema: { + type: "object", + properties: { + limit: { + type: "integer", + minimum: LIST_LIMIT_MIN, + maximum: LIST_LIMIT_MAX, + description: `Max events to return (${LIST_LIMIT_MIN}–${LIST_LIMIT_MAX})`, + }, + }, + additionalProperties: false, + }, + }, + { + name: "memory_feed", + description: + "Pull new memory versions after a cursor (capture feed). " + + "Returns { entries, nextCursor }. Always set exclude_generator to your " + + "generator_agent_id (e.g. resident-distiller) for loop-safety. " + + "Copy accessTags from each entry onto distilled writes — never widen.", + inputSchema: { + type: "object", + properties: { + after: { + type: "integer", + minimum: 0, + description: "Exclusive cursor (feed_seq > after). Default 0.", + }, + limit: { + type: "integer", + minimum: 1, + maximum: 100, + description: "Page size (1–100, default 50)", + }, + exclude_generator: { + type: "string", + description: "Skip versions written by this generator_agent_id (loop-safety)", + }, + }, + additionalProperties: false, + }, + }, +]; diff --git a/src/tools/add.ts b/src/tools/add.ts deleted file mode 100644 index 82b4c8e..0000000 --- a/src/tools/add.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { AddRequest, parseWithArk } from "../http-bodies.ts"; -import type { MemoryAddBody } from "./client.ts"; -import { defineMemoryHttpTool } from "./install.ts"; - -function parseAddArgs(args: Record): MemoryAddBody { - const parsed = parseWithArk(AddRequest, args, "memory_add"); - // Forward only schema fields so identity keys never ride the wire. - const body: MemoryAddBody = { - title: parsed.title, - text: parsed.text, - }; - if (parsed.access_tags !== undefined) { - body.access_tags = parsed.access_tags; - } - if (parsed.kind !== undefined) { - body.kind = parsed.kind; - } - if (parsed.generator_agent_id !== undefined) { - body.generator_agent_id = parsed.generator_agent_id; - } - if (parsed.provenance !== undefined) { - body.provenance = parsed.provenance; - } - if (parsed.lineage_class !== undefined) { - body.lineage_class = parsed.lineage_class; - } - if (parsed.temporal_class !== undefined) { - body.temporal_class = parsed.temporal_class; - } - if (parsed.derived_from !== undefined) { - body.derived_from = parsed.derived_from; - } - if (parsed.valid_from !== undefined) { - body.valid_from = parsed.valid_from; - } - if (parsed.valid_until !== undefined) { - body.valid_until = parsed.valid_until; - } - if (parsed.share !== undefined) { - // Rebuild share field-by-field — arktype keeps undeclared nested keys. - const share: NonNullable = {}; - if (parsed.share.tenant !== undefined) { - share.tenant = parsed.share.tenant; - } - if (parsed.share.principals !== undefined) { - share.principals = parsed.share.principals; - } - if (parsed.share.tags !== undefined) { - share.tags = parsed.share.tags; - } - body.share = share; - } - return body; -} - -/** - * Installable tool: POST /api/tenants/:tenantId/memory/add. - * - * Tenant and auth come from env (`memoryTenantId`, `memoryAuthToken`); - * model args never carry identity. Distiller claims pass - * `generator_agent_id` + `derived_from` for loop-safety and lineage. - */ -export const memoryAdd = defineMemoryHttpTool({ - id: "@corbits/memory/add", - name: "memory_add", - description: - "Store a note in tenant memory. Returns { documentId, versionId }. " + - "For distilled claims set generator_agent_id, provenance=inferred, " + - "lineage_class=derived, and derived_from source version ids. " + - "Identity is the authenticated principal on the hub.", - inputSchema: { - type: "object", - properties: { - title: { - type: "string", - description: "Short title for the document", - }, - text: { - type: "string", - description: "Full body text to store", - }, - access_tags: { - type: "array", - items: { type: "string" }, - description: - "Optional grant-pattern tags controlling document visibility " + - "(distiller: copy from source feed entry, never widen)", - }, - kind: { - type: "string", - description: "Document kind (default note)", - }, - generator_agent_id: { - type: "string", - description: - "Agent id that authored this version (e.g. resident-distiller). " + - "Enables feed excludeGenerator loop-safety.", - }, - provenance: { - type: "string", - enum: ["stated", "inferred", "unknown"], - description: "How content was obtained (inferred for distilled claims)", - }, - lineage_class: { - type: "string", - enum: ["native", "imported", "derived"], - description: "Data lineage (derived for distilled claims)", - }, - temporal_class: { - type: "string", - enum: ["event", "deadline", "state", "lesson"], - description: "Temporal ranking class", - }, - derived_from: { - type: "array", - items: { type: "string" }, - description: "Source version ids this claim is derived from", - }, - valid_from: { - type: "string", - description: "Optional validity start (ISO)", - }, - valid_until: { - type: "string", - description: "Optional validity end (ISO)", - }, - share: { - type: "object", - properties: { - tenant: { type: "boolean" }, - principals: { - type: "array", - items: { type: "string" }, - }, - tags: { - type: "array", - items: { type: "string" }, - }, - }, - description: - "Optional share sugar that mints access tags (tenant / principals / tags)", - }, - }, - required: ["title", "text"], - additionalProperties: false, - }, - async handle(client, args, signal) { - const body = parseAddArgs(args); - const result = await client.add(body, signal); - return JSON.stringify(result); - }, -}); diff --git a/src/tools/feed.ts b/src/tools/feed.ts deleted file mode 100644 index 795b8e4..0000000 --- a/src/tools/feed.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type } from "arktype"; - -import { parseWithArk } from "../http-bodies.ts"; -import { defineMemoryHttpTool } from "./install.ts"; - -const FeedArgs = type({ - "after?": "number.integer >= 0", - "limit?": "1 <= number.integer <= 100", - "exclude_generator?": "string", -}); - -function coerceFeedArgs(args: Record): Record { - const out = { ...args }; - for (const key of ["after", "limit"] as const) { - const raw = out[key]; - if (typeof raw === "string" && raw.trim() !== "") { - const n = Number(raw); - if (Number.isFinite(n)) out[key] = n; - } - } - return out; -} - -/** - * Installable tool: GET /api/tenants/:tenantId/memory/feed. - * - * Distiller pull surface — always pass exclude_generator matching the - * writer's generator_agent_id so the agent never re-consumes its own claims. - */ -export const memoryFeed = defineMemoryHttpTool({ - id: "@corbits/memory/feed", - name: "memory_feed", - description: - "Pull new memory versions after a cursor (capture feed). " + - "Returns { entries, nextCursor }. Always set exclude_generator to your " + - "generator_agent_id (e.g. resident-distiller) for loop-safety. " + - "Copy accessTags from each entry onto distilled writes — never widen.", - inputSchema: { - type: "object", - properties: { - after: { - type: "integer", - minimum: 0, - description: "Exclusive cursor (feed_seq > after). Default 0.", - }, - limit: { - type: "integer", - minimum: 1, - maximum: 100, - description: "Page size (1–100, default 50)", - }, - exclude_generator: { - type: "string", - description: - "Skip versions written by this generator_agent_id (loop-safety)", - }, - }, - additionalProperties: false, - }, - async handle(client, args, signal) { - const parsed = parseWithArk( - FeedArgs, - coerceFeedArgs(args), - "memory_feed", - ); - const result = await client.feed( - { - ...(parsed.after !== undefined ? { after: parsed.after } : {}), - ...(parsed.limit !== undefined ? { limit: parsed.limit } : {}), - ...(parsed.exclude_generator !== undefined - ? { excludeGenerator: parsed.exclude_generator } - : {}), - }, - signal, - ); - return JSON.stringify(result); - }, -}); diff --git a/src/tools/index.ts b/src/tools/index.ts deleted file mode 100644 index 686a0a5..0000000 --- a/src/tools/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Interchange `defineTool` factories for hub memory routes. - * - * Install on a workflow like any other open tool package. Each factory - * requires env: `memoryBaseUrl`, `memoryTenantId`, `memoryAuthToken`. - * Tools call `/api/tenants/:tenantId/memory/*` over HTTP — no plane DI. - */ - -export { memoryAdd } from "./add.ts"; -export { memorySearch } from "./search.ts"; -export { memoryList } from "./list.ts"; -export { memoryFeed } from "./feed.ts"; -export { - createMemoryHttpClient, - MEMORY_TOOL_ENV_KEYS, - type MemoryHttpClient, - type MemoryHttpConfig, - type MemoryToolEnv, -} from "./client.ts"; - -/** Re-export installer grant requirements (same as package root). */ -export { - capabilityIdsForSurface, - MEMORY_CAPABILITY_IDS, - MEMORY_GRANT_REQUIREMENTS, - type MemoryGrantRequirement, - type MemoryGrantInstallHint, - type MemoryGrantSurface, -} from "../grant-requirements.ts"; - diff --git a/src/tools/install.ts b/src/tools/install.ts deleted file mode 100644 index a098739..0000000 --- a/src/tools/install.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - createToolRunner, - defineTool, - stringTool, - type BaseEnv, -} from "@intx/agent"; - -import { - createMemoryHttpClient, - MEMORY_TOOL_ENV_KEYS, - readMemoryToolEnv, - type MemoryHttpClient, - type MemoryToolEnv, -} from "./client.ts"; - -export type MemoryInstallEnv = BaseEnv & MemoryToolEnv; - -type JSONSchemaObject = { - type: "object"; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; -}; - -/** - * Shared defineTool shell for memory HTTP tools. - * Credentials from env; model args never carry identity. - */ -export function defineMemoryHttpTool(opts: { - id: string; - name: string; - description: string; - inputSchema: JSONSchemaObject; - handle: ( - client: MemoryHttpClient, - args: Record, - signal: AbortSignal | undefined, - ) => Promise; -}) { - // Bound to a const rather than passed as a fresh object literal: some - // `@intx/agent` versions add a static `definitions` field to defineTool's - // options (so callers can enumerate tool names without instantiating the - // factory) that this package's own pinned version predates. Routing - // through a named variable gets normal structural (duck-typed) parameter - // assignment instead of TypeScript's excess-property check on literals, - // so this call type-checks against either shape — the extra field is - // simply unused by an older defineTool at runtime. - const toolOpts = { - id: opts.id, - requires: MEMORY_TOOL_ENV_KEYS, - definitions: [{ name: opts.name }], - factory(env: MemoryInstallEnv) { - const client = createMemoryHttpClient(readMemoryToolEnv(env)); - const runner = createToolRunner([ - stringTool({ - definition: { - name: opts.name, - description: opts.description, - inputSchema: opts.inputSchema, - }, - handler: async (args, signal) => - opts.handle(client, args, signal), - }), - ]); - return { - definitions: runner.definitions, - run: runner.run.bind(runner), - }; - }, - }; - return defineTool(toolOpts); -} diff --git a/src/tools/list.ts b/src/tools/list.ts deleted file mode 100644 index bf8c328..0000000 --- a/src/tools/list.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - coerceOptionalLimitArg, - ListArgs, - parseWithArk, -} from "../http-bodies.ts"; -import { LIST_LIMIT_MAX, LIST_LIMIT_MIN } from "../limits.ts"; -import { defineMemoryHttpTool } from "./install.ts"; - -function parseListLimit(args: Record): number | undefined { - const parsed = parseWithArk( - ListArgs, - coerceOptionalLimitArg(args), - "memory_list", - ); - return parsed.limit; -} - -/** - * Installable tool: GET /api/tenants/:tenantId/memory/list. - * - * Tenant and auth come from env; model args never carry identity. - */ -export const memoryList = defineMemoryHttpTool({ - id: "@corbits/memory/list", - name: "memory_list", - description: - "List recent documents visible to the authenticated principal " + - "in this tenant's memory.", - inputSchema: { - type: "object", - properties: { - limit: { - type: "integer", - minimum: LIST_LIMIT_MIN, - maximum: LIST_LIMIT_MAX, - description: `Max events to return (${LIST_LIMIT_MIN}–${LIST_LIMIT_MAX})`, - }, - }, - additionalProperties: false, - }, - async handle(client, args, signal) { - const limit = parseListLimit(args); - const result = await client.list(limit, signal); - return JSON.stringify(result); - }, -}); diff --git a/src/tools/search.ts b/src/tools/search.ts deleted file mode 100644 index 6814517..0000000 --- a/src/tools/search.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - coerceOptionalLimitArg, - parseWithArk, - SearchRequest, -} from "../http-bodies.ts"; -import { SEARCH_LIMIT_MAX, SEARCH_LIMIT_MIN } from "../limits.ts"; -import type { MemorySearchBody } from "./client.ts"; -import { defineMemoryHttpTool } from "./install.ts"; - -function parseSearchArgs(args: Record): MemorySearchBody { - const parsed = parseWithArk( - SearchRequest, - coerceOptionalLimitArg(args), - "memory_search", - ); - const body: MemorySearchBody = { query: parsed.query }; - if (parsed.limit !== undefined) body.limit = parsed.limit; - if (parsed.kinds !== undefined) body.kinds = parsed.kinds; - if (parsed.entity_ids !== undefined) body.entity_ids = parsed.entity_ids; - if (parsed.sources !== undefined) body.sources = parsed.sources; - if (parsed.includeEvidence !== undefined) { - body.includeEvidence = parsed.includeEvidence; - } - if (parsed.includeDeprecated !== undefined) { - body.includeDeprecated = parsed.includeDeprecated; - } - return body; -} - -/** - * Installable tool: POST /api/tenants/:tenantId/memory/search. - * - * Tenant and auth come from env; model args never carry identity. - */ -export const memorySearch = defineMemoryHttpTool({ - id: "@corbits/memory/search", - name: "memory_search", - description: - "Hybrid semantic + keyword search over tenant memory. " + - "Returns ranked items with optional additive attribution " + - "(provenance, temporal class, corroboration, derivedFrom) and evidence. " + - "Attribute stated content to the actor; treat inferred as own-voice claims. " + - "Identity is the authenticated principal on the hub.", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "Search query text", - }, - limit: { - type: "integer", - minimum: SEARCH_LIMIT_MIN, - maximum: SEARCH_LIMIT_MAX, - description: `Max hits to return (${SEARCH_LIMIT_MIN}–${SEARCH_LIMIT_MAX})`, - }, - kinds: { - type: "array", - items: { type: "string" }, - description: "Optional document kind filter", - }, - entity_ids: { - type: "array", - items: { type: "string" }, - description: "Optional entity-id filter", - }, - sources: { - type: "array", - items: { type: "string" }, - description: - 'Optional channel filter (e.g. "local" and/or live source ids)', - }, - includeEvidence: { - type: "boolean", - description: - "Include evidence strength on the response (hub default true)", - }, - includeDeprecated: { - type: "boolean", - description: - "Include deprecated versions in results (default false)", - }, - }, - required: ["query"], - additionalProperties: false, - }, - async handle(client, args, signal) { - const body = parseSearchArgs(args); - const result = await client.search(body, signal); - return JSON.stringify(result); - }, -}); diff --git a/src/tools/tools.test.ts b/src/tools/tools.test.ts deleted file mode 100644 index 75bc4e1..0000000 --- a/src/tools/tools.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import type { BaseEnv } from "@intx/agent"; - -import { memoryAdd } from "./add.ts"; -import { memorySearch } from "./search.ts"; -import { memoryList } from "./list.ts"; -import { - createMemoryHttpClient, - MEMORY_TOOL_ENV_KEYS, - readMemoryToolEnv, - type MemoryToolEnv, -} from "./client.ts"; - -const BASE = "https://hub.example"; -const TENANT = "tenant-abc"; -const TOKEN = "tok-xyz"; - -/** Factories only read memory* keys; cast a minimal env for tests. */ -function toolEnv(overrides?: Partial): BaseEnv & MemoryToolEnv { - return { - memoryBaseUrl: overrides?.memoryBaseUrl ?? BASE, - memoryTenantId: overrides?.memoryTenantId ?? TENANT, - memoryAuthToken: overrides?.memoryAuthToken ?? TOKEN, - ...(overrides?.memoryFetch !== undefined - ? { memoryFetch: overrides.memoryFetch } - : {}), - } as BaseEnv & MemoryToolEnv; -} - -type Captured = { - url: string; - method: string; - headers: Headers; - body: string | null; -}; - -function makeFetchMock( - respond: (req: Captured) => { status: number; json?: unknown; text?: string }, -) { - const calls: Captured[] = []; - const fetchMock = mock( - async (input: string | URL | Request, init?: RequestInit) => { - const url = - typeof input === "string" - ? input - : input instanceof URL - ? input.href - : input.url; - const method = init?.method ?? "GET"; - const headers = new Headers(init?.headers); - const body = - typeof init?.body === "string" - ? init.body - : init?.body == null - ? null - : String(init.body); - const captured: Captured = { url, method, headers, body }; - calls.push(captured); - const r = respond(captured); - if (r.text !== undefined) { - return new Response(r.text, { - status: r.status, - headers: { "Content-Type": "text/plain" }, - }); - } - return new Response(JSON.stringify(r.json ?? {}), { - status: r.status, - headers: { "Content-Type": "application/json" }, - }); - }, - ); - return { calls, fetchMock: fetchMock as unknown as typeof fetch }; -} - -describe("MEMORY_TOOL_ENV_KEYS", () => { - test("lists the three credential keys", () => { - expect([...MEMORY_TOOL_ENV_KEYS]).toEqual([ - "memoryBaseUrl", - "memoryTenantId", - "memoryAuthToken", - ]); - }); -}); - -describe("readMemoryToolEnv", () => { - test("rejects empty base url", () => { - expect(() => - readMemoryToolEnv({ - memoryBaseUrl: "", - memoryTenantId: TENANT, - memoryAuthToken: TOKEN, - }), - ).toThrow(/memoryBaseUrl/); - }); - - test("rejects empty tenant and token", () => { - expect(() => - readMemoryToolEnv({ - memoryBaseUrl: BASE, - memoryTenantId: "", - memoryAuthToken: TOKEN, - }), - ).toThrow(/memoryTenantId/); - expect(() => - readMemoryToolEnv({ - memoryBaseUrl: BASE, - memoryTenantId: TENANT, - memoryAuthToken: "", - }), - ).toThrow(/memoryAuthToken/); - }); -}); - -describe("createMemoryHttpClient", () => { - test("POSTs add under tenant path with Bearer auth", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { documentId: "doc-1", versionId: "ver-1" }, - })); - const client = createMemoryHttpClient({ - baseUrl: `${BASE}///`, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - const out = await client.add({ title: "t", text: "body" }); - expect(out).toEqual({ documentId: "doc-1", versionId: "ver-1" }); - expect(calls).toHaveLength(1); - const c = calls[0]!; - expect(c.method).toBe("POST"); - expect(c.url).toBe(`${BASE}/api/tenants/${TENANT}/memory/add`); - expect(c.headers.get("Authorization")).toBe(`Bearer ${TOKEN}`); - expect(c.headers.get("Content-Type")).toBe("application/json"); - const parsed = JSON.parse(c.body ?? "{}") as Record; - expect(parsed).toEqual({ title: "t", text: "body" }); - expect(parsed).not.toHaveProperty("tenantId"); - expect(parsed).not.toHaveProperty("principalId"); - }); - - test("GET list with limit query", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { events: [] }, - })); - const client = createMemoryHttpClient({ - baseUrl: BASE, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - await client.list(5); - expect(calls[0]!.method).toBe("GET"); - expect(calls[0]!.url).toBe( - `${BASE}/api/tenants/${TENANT}/memory/list?limit=5`, - ); - expect(calls[0]!.body).toBeNull(); - }); - - test("surfaces non-2xx as Error", async () => { - const { fetchMock } = makeFetchMock(() => ({ - status: 403, - json: { error: "forbidden" }, - })); - const client = createMemoryHttpClient({ - baseUrl: BASE, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - await expect(client.search({ query: "x" })).rejects.toThrow( - /memory HTTP 403/, - ); - }); - - test("clips long error response bodies", async () => { - const long = "e".repeat(800); - const { fetchMock } = makeFetchMock(() => ({ - status: 500, - text: long, - })); - const client = createMemoryHttpClient({ - baseUrl: BASE, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - try { - await client.list(); - expect.unreachable("expected throw"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - expect(msg).toMatch(/memory HTTP 500:/); - expect(msg.length).toBeLessThan(600); - expect(msg.endsWith("…")).toBe(true); - } - }); - - test("rejects invalid JSON on 2xx", async () => { - const { fetchMock } = makeFetchMock(() => ({ - status: 200, - text: "not-json", - })); - const client = createMemoryHttpClient({ - baseUrl: BASE, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - await expect(client.list()).rejects.toThrow(/invalid JSON/); - }); - - test("empty 2xx body becomes {}", async () => { - const { fetchMock } = makeFetchMock(() => ({ - status: 200, - text: "", - })); - const client = createMemoryHttpClient({ - baseUrl: BASE, - tenantId: TENANT, - authToken: TOKEN, - fetch: fetchMock, - }); - expect(await client.list()).toEqual({}); - }); -}); - -describe("memoryAdd factory", () => { - test("declares id and requires", () => { - expect(memoryAdd.id).toBe("@corbits/memory/add"); - expect([...memoryAdd.requires]).toEqual([...MEMORY_TOOL_ENV_KEYS]); - }); - - test("happy path: body has no identity fields", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { documentId: "doc-9", versionId: "ver-9" }, - })); - const bundle = memoryAdd(toolEnv({ memoryFetch: fetchMock })); - expect(bundle.definitions.map((d) => d.name)).toEqual(["memory_add"]); - const result = await bundle.run( - { - id: "call-1", - name: "memory_add", - arguments: { title: "note", text: "hello" }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - expect(result.content).toBe(JSON.stringify({ documentId: "doc-9", versionId: "ver-9" })); - const body = JSON.parse(calls[0]!.body ?? "{}") as Record; - expect(body).not.toHaveProperty("tenantId"); - expect(body).not.toHaveProperty("principalId"); - expect(body).not.toHaveProperty("tenant_id"); - expect(body).not.toHaveProperty("principal_id"); - }); - - test("strips adversarial identity args from wire body", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { documentId: "doc-x" }, - })); - const bundle = memoryAdd(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-adv", - name: "memory_add", - arguments: { - title: "note", - text: "hello", - tenantId: "evil-tenant", - principalId: "evil-principal", - tenant_id: "evil", - principal_id: "evil", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - const body = JSON.parse(calls[0]!.body ?? "{}") as Record; - expect(body).toEqual({ title: "note", text: "hello" }); - expect(calls[0]!.url).toContain(`/tenants/${TENANT}/`); - }); - - test("rebuilds share without nested extras", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { documentId: "doc-share" }, - })); - const bundle = memoryAdd(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-share", - name: "memory_add", - arguments: { - title: "note", - text: "hello", - share: { - tenant: true, - principalId: "nested-evil", - authToken: "should-not-wire", - tags: ["team:eng"], - }, - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - const body = JSON.parse(calls[0]!.body ?? "{}") as { - share?: Record; - }; - expect(body.share).toEqual({ tenant: true, tags: ["team:eng"] }); - expect(body.share).not.toHaveProperty("principalId"); - expect(body.share).not.toHaveProperty("authToken"); - }); - - test("rejects empty title", async () => { - const { fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { documentId: "x" }, - })); - const bundle = memoryAdd(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-bad", - name: "memory_add", - arguments: { title: "", text: "body" }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - }); -}); - -describe("memorySearch factory", () => { - test("POSTs search with query only", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { items: [], evidence: "none" }, - })); - const bundle = memorySearch(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-2", - name: "memory_search", - arguments: { query: "standup notes" }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - expect(calls[0]!.url).toBe( - `${BASE}/api/tenants/${TENANT}/memory/search`, - ); - expect(JSON.parse(calls[0]!.body ?? "{}")).toEqual({ - query: "standup notes", - }); - }); - - test("coerces string limit and rejects out-of-range", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { items: [] }, - })); - const bundle = memorySearch(toolEnv({ memoryFetch: fetchMock })); - const ok = await bundle.run( - { - id: "call-lim", - name: "memory_search", - arguments: { query: "q", limit: "5" }, - }, - new AbortController().signal, - ); - expect(ok.isError).toBeFalsy(); - expect(JSON.parse(calls[0]!.body ?? "{}")).toEqual({ - query: "q", - limit: 5, - }); - - const bad = await bundle.run( - { - id: "call-lim-bad", - name: "memory_search", - arguments: { query: "q", limit: 99 }, - }, - new AbortController().signal, - ); - expect(bad.isError).toBe(true); - }); - - test("strips adversarial identity args from search wire body", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { items: [] }, - })); - const bundle = memorySearch(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-search-adv", - name: "memory_search", - arguments: { - query: "q", - tenantId: "evil-tenant", - principalId: "evil-principal", - authToken: "nope", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - const body = JSON.parse(calls[0]!.body ?? "{}") as Record; - expect(body).toEqual({ query: "q" }); - expect(calls[0]!.url).toContain(`/tenants/${TENANT}/`); - }); -}); - -describe("memoryList factory", () => { - test("GETs list without identity in query", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { - events: [ - { - at: "2026-01-01", - title: "a", - source: "local", - tenantId: TENANT, - principalId: "p", - }, - ], - }, - })); - const bundle = memoryList(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-3", - name: "memory_list", - arguments: { limit: 10 }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - expect(calls[0]!.url).toContain("/memory/list?limit=10"); - expect(calls[0]!.url).not.toContain("principal"); - }); - - test("ignores adversarial identity args on list", async () => { - const { calls, fetchMock } = makeFetchMock(() => ({ - status: 200, - json: { events: [] }, - })); - const bundle = memoryList(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { - id: "call-list-adv", - name: "memory_list", - arguments: { - limit: 3, - tenantId: "evil", - principalId: "evil", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - expect(calls[0]!.url).toBe( - `${BASE}/api/tenants/${TENANT}/memory/list?limit=3`, - ); - expect(calls[0]!.url).not.toContain("evil"); - }); - - test("HTTP error becomes isError tool result", async () => { - const { fetchMock } = makeFetchMock(() => ({ - status: 401, - json: { error: "unauthorized" }, - })); - const bundle = memoryList(toolEnv({ memoryFetch: fetchMock })); - const result = await bundle.run( - { id: "call-4", name: "memory_list", arguments: {} }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - }); -}); diff --git a/src/workflow-mount.test.ts b/src/workflow-mount.test.ts new file mode 100644 index 0000000..cf9b058 --- /dev/null +++ b/src/workflow-mount.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, test } from "bun:test"; +import { Hono } from "hono"; + +import type { Memory } from "./memory.ts"; +import { + mountWorkflowMemory, + type AgentTokenAuth, + type ResolvedWorkflowRunScope, + type WorkflowMemoryEnv, +} from "./workflow-mount.ts"; + +const RUN_SCOPE: ResolvedWorkflowRunScope = { + tenantId: "acme", + principalId: "agent-1", + runId: "run-1", +}; + +const ADDRESS = "run-1@acme.example.com"; +const AGENT_TOKEN = "agent-token"; + +const agentHeaders = { + authorization: `Bearer ${AGENT_TOKEN}`, + "x-workflow-run-address": ADDRESS, +}; + +function fakeMemory(seen: Record[]): Memory { + type Params = Record; + return { + capabilities: { embeddingsConfigured: false }, + add: async (params: Params) => { + seen.push({ verb: "add", ...params }); + return { documentId: "doc_1", versionId: "ver_1" }; + }, + search: async (params: Params) => { + seen.push({ verb: "search", ...params }); + return { items: [] }; + }, + list: async (params: Params) => { + seen.push({ verb: "list", ...params }); + return []; + }, + feed: async (params: Params) => { + seen.push({ verb: "feed", ...params }); + return { entries: [], nextCursor: null }; + }, + close: async () => {}, + } as unknown as Memory; +} + +function agentTokenAuth(overrides: Partial = {}): AgentTokenAuth { + return { + verify: (ctx) => { + const c = ctx as { req: { header(name: string): string | undefined } }; + return c.req.header("authorization") === `Bearer ${AGENT_TOKEN}` + ? { tenantId: "acme", definitionId: "def-1" } + : undefined; + }, + resolveRun: (address) => (address === ADDRESS ? RUN_SCOPE : null), + ...overrides, + }; +} + +function host(seen: Record[], agentToken: AgentTokenAuth = agentTokenAuth()) { + return mountWorkflowMemory(new Hono(), { + memory: fakeMemory(seen), + agentToken, + }); +} + +describe("agent-token authentication", () => { + test("an agent bearer authenticates and scopes to the run's tenant", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/list", { headers: agentHeaders }); + expect(res.status).toBe(200); + expect(seen[0]).toMatchObject({ verb: "list", tenantId: "acme", principalId: "agent-1" }); + }); + + test("no bearer at all is refused", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/list", { + headers: { "x-workflow-run-address": ADDRESS }, + }); + expect(res.status).toBe(401); + expect(seen).toHaveLength(0); + }); + + test("a token from another tenant is refused", async () => { + const seen: Record[] = []; + const app = host( + seen, + agentTokenAuth({ verify: () => ({ tenantId: "other", definitionId: "def-1" }) }), + ); + const res = await app.request("/list", { headers: agentHeaders }); + expect(res.status).toBe(401); + expect(seen).toHaveLength(0); + }); + + test("an address that names no run is refused", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/list", { + headers: { ...agentHeaders, "x-workflow-run-address": "run-9@acme.example.com" }, + }); + expect(res.status).toBe(401); + expect(seen).toHaveLength(0); + }); +}); + +describe("the routes the memory tools call", () => { + test("add scopes to the run and never takes identity from the body", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/add", { + method: "POST", + headers: { "content-type": "application/json", ...agentHeaders }, + body: JSON.stringify({ title: "T", text: "b", tenantId: "spoofed" }), + }); + expect(res.status).toBe(200); + expect(seen[0]).toMatchObject({ + verb: "add", + tenantId: "acme", + principalId: "agent-1", + content: { title: "T", text: "b" }, + }); + }); + + test("search forwards the parsed filters", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/search", { + method: "POST", + headers: { "content-type": "application/json", ...agentHeaders }, + body: JSON.stringify({ query: "q", limit: 3, entity_ids: ["e1"] }), + }); + expect(res.status).toBe(200); + expect(seen[0]).toMatchObject({ verb: "search", query: "q", limit: 3, entityIds: ["e1"] }); + }); + + test("feed forwards the cursor and generator exclusion", async () => { + const seen: Record[] = []; + const res = await host(seen).request( + "/feed?after=4&limit=10&exclude_generator=resident-distiller", + { headers: agentHeaders }, + ); + expect(res.status).toBe(200); + expect(seen[0]).toMatchObject({ + verb: "feed", + after: 4, + limit: 10, + excludeGenerator: "resident-distiller", + }); + }); + + test("a malformed add body is rejected before the plane is touched", async () => { + const seen: Record[] = []; + const res = await host(seen).request("/add", { + method: "POST", + headers: { "content-type": "application/json", ...agentHeaders }, + body: JSON.stringify({ title: "T" }), + }); + expect(res.status).toBe(400); + expect(seen).toHaveLength(0); + }); +}); diff --git a/src/workflow-mount.ts b/src/workflow-mount.ts new file mode 100644 index 0000000..e5a25f2 --- /dev/null +++ b/src/workflow-mount.ts @@ -0,0 +1,220 @@ +/** + * Run-scoped variant of the memory routes for a host whose callers are + * deployed agents rather than browser sessions: there is no `TenantEnv` + * principal on the context, only a hub-minted bearer plus the run address it + * is acting for. The human-facing tenant routes keep their session auth + * untouched — mixing two unrelated auth conventions into one mount would + * make each harder to reason about, so this is a second, parallel mount a + * host wires up only when it actually runs agents. + * + * Authorization is the token itself: the hub only mints an agent token for a + * definition it already authorized, and every call here is confined to the + * verified run's tenant and principal. This mount runs no grant check of its + * own and has no tenant override. + */ +import { type } from "arktype"; +import type { Hono, MiddlewareHandler } from "hono"; + +import { + AddRequest, + parseFeedQuery, + parseListLimitString, + SearchRequest, +} from "./http-bodies.ts"; +import { formatCaughtError, log } from "./log.ts"; +import { MemoryError, type Memory } from "./memory.ts"; +import type { ShareSugar } from "./grant-tags.ts"; + +/** Who a run authenticates as, and which run it is acting on behalf of. */ +export type ResolvedWorkflowRunScope = { + readonly tenantId: string; + readonly principalId: string; + readonly runId: string; +}; + +/** What a verified agent token proves. Structural, so this package never + * depends on the library that mints one. */ +export type AgentTokenIdentity = { + readonly tenantId: string; + readonly definitionId: string; +}; + +/** + * The host's two halves of agent authentication. `verify` reads the + * presented `Authorization` header and returns `undefined` when it is not an + * agent token; `resolveRun` is the host's existing lookup from the + * `x-workflow-run-address` header to the run. A token whose tenant is not + * the resolved run's tenant is refused, so a bearer minted for one workbench + * cannot act on another's run. + */ +export type AgentTokenAuth = { + verify: ( + ctx: unknown, + ) => Promise | AgentTokenIdentity | undefined; + resolveRun: ( + runAddress: string, + ) => Promise | ResolvedWorkflowRunScope | null; +}; + +export type WorkflowMemoryEnv = { + Variables: { workflowRunScope: ResolvedWorkflowRunScope }; +}; + +export type MountWorkflowMemoryOpts = { + memory: Memory; + agentToken: AgentTokenAuth; +}; + +/** + * Mount the run-scoped memory routes onto a host Hono app. Every route sits + * behind the bearer middleware — there is no unauthenticated case, since an + * agent run always presents credentials. + */ +export function mountWorkflowMemory( + app: Hono, + opts: MountWorkflowMemoryOpts, +): Hono { + const { memory, agentToken } = opts; + + const authenticate: MiddlewareHandler = async (c, next) => { + const identity = await agentToken.verify(c); + const address = c.req.header("x-workflow-run-address") ?? ""; + const scope = identity === undefined ? null : await agentToken.resolveRun(address); + // Same 401 whether the bearer was unrecognized, the address named no run, + // or the run belongs to another tenant: a bearer learns nothing from the + // difference. + if (identity === undefined || scope === null || scope.tenantId !== identity.tenantId) { + return c.json( + { error: "Missing or unrecognized bearer token / run address" }, + 401, + ); + } + c.set("workflowRunScope", scope); + await next(); + return undefined; + }; + app.use("*", authenticate); + + function failure(c: Parameters>[0], verb: string, err: unknown) { + if (err instanceof MemoryError) { + return c.json({ error: err.message }, err.status as 400 | 501); + } + const message = formatCaughtError(err); + log.error(`memory ${verb} failed: ${message}`, { error: message }); + return c.json({ error: `${verb} failed` }, 502); + } + + app.post("/add", async (c) => { + let raw: unknown; + try { + raw = await c.req.json(); + } catch { + return c.json({ error: "Invalid JSON body" }, 400); + } + const body = AddRequest(raw); + if (body instanceof type.errors) return c.json({ error: body.summary }, 400); + + const scope = c.get("workflowRunScope"); + try { + const result = await memory.add({ + content: { title: body.title, text: body.text }, + tenantId: scope.tenantId, + principalId: scope.principalId, + ...(body.access_tags !== undefined ? { accessTags: body.access_tags } : {}), + ...(body.share !== undefined ? { share: body.share as ShareSugar } : {}), + ...(body.kind !== undefined ? { kind: body.kind } : {}), + ...(body.generator_agent_id !== undefined + ? { generatorAgentId: body.generator_agent_id } + : {}), + ...(body.provenance !== undefined ? { provenance: body.provenance } : {}), + ...(body.lineage_class !== undefined ? { lineageClass: body.lineage_class } : {}), + ...(body.temporal_class !== undefined ? { temporalClass: body.temporal_class } : {}), + ...(body.derived_from !== undefined ? { derivedFrom: body.derived_from } : {}), + ...(body.valid_from !== undefined ? { validFrom: body.valid_from } : {}), + ...(body.valid_until !== undefined ? { validUntil: body.valid_until } : {}), + }); + return c.json({ data: result }); + } catch (err) { + return failure(c, "add", err); + } + }); + + app.post("/search", async (c) => { + let raw: unknown; + try { + raw = await c.req.json(); + } catch { + return c.json({ error: "Invalid JSON body" }, 400); + } + const body = SearchRequest(raw); + if (body instanceof type.errors) return c.json({ error: body.summary }, 400); + + const scope = c.get("workflowRunScope"); + try { + const result = await memory.search({ + query: body.query, + tenantId: scope.tenantId, + principalId: scope.principalId, + ...(body.limit !== undefined ? { limit: body.limit } : {}), + ...(body.kinds !== undefined ? { kinds: body.kinds } : {}), + ...(body.entity_ids !== undefined ? { entityIds: body.entity_ids } : {}), + ...(body.sources !== undefined ? { sources: body.sources } : {}), + ...(body.includeEvidence !== undefined + ? { includeEvidence: body.includeEvidence } + : {}), + ...(body.includeDeprecated !== undefined + ? { includeDeprecated: body.includeDeprecated } + : {}), + }); + return c.json({ data: result }); + } catch (err) { + return failure(c, "search", err); + } + }); + + app.get("/list", async (c) => { + const limit = parseListLimitString(c.req.query("limit")); + if (limit === null) return c.json({ error: "limit is out of range" }, 400); + + const scope = c.get("workflowRunScope"); + try { + const events = await memory.list({ + tenantId: scope.tenantId, + principalId: scope.principalId, + ...(limit !== undefined ? { limit } : {}), + }); + return c.json({ data: events }); + } catch (err) { + return failure(c, "list", err); + } + }); + + app.get("/feed", async (c) => { + if (memory.feed === undefined) { + return c.json({ error: "feed is not available on this memory plane" }, 501); + } + const after = c.req.query("after"); + const limit = c.req.query("limit"); + const excludeGenerator = c.req.query("exclude_generator"); + const parsed = parseFeedQuery({ + ...(after !== undefined ? { after } : {}), + ...(limit !== undefined ? { limit } : {}), + ...(excludeGenerator !== undefined ? { exclude_generator: excludeGenerator } : {}), + }); + if (!parsed.ok) return c.json({ error: parsed.error }, 400); + + const scope = c.get("workflowRunScope"); + try { + const result = await memory.feed({ + tenantId: scope.tenantId, + principalId: scope.principalId, + ...parsed.value, + }); + return c.json({ data: result }); + } catch (err) { + return failure(c, "feed", err); + } + }); + + return app; +}