@adonis-agora/agent 0.25.1 → 0.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/stubs/config/agent.stub +8 -8
- package/dist/stubs/config/mcp.stub +5 -5
- package/dist/stubs/database/migrations/create_agent_rag_chunks.stub +10 -10
- package/dist/stubs/database/migrations/create_agent_tables.stub +14 -14
- package/package.json +5 -2
- package/skills/agent-governance/SKILL.md +233 -0
- package/skills/agent-personas-agents/SKILL.md +220 -0
- package/skills/agent-setup/SKILL.md +273 -0
- package/skills/agent-testing/SKILL.md +236 -0
- package/skills/agent-tools/SKILL.md +270 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @adonis-agora/agent
|
|
2
2
|
|
|
3
|
+
## 0.25.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#82](https://github.com/DavideCarvalho/adonis-agora-agent/pull/82) [`59be4e9`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/59be4e9943b47d79ce5f006c1d24b9b39e052f84) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add TanStack Intent agent skills. Both packages now ship a `skills/` directory
|
|
8
|
+
(five skills under @adonis-agora/agent: setup, tools, governance, personas &
|
|
9
|
+
multi-agent delegation, offline testing; one under @adonis-agora/agent-dashboard:
|
|
10
|
+
the governance console client) that coding agents can load as structured,
|
|
11
|
+
verified documentation. The directories are included in each package's `files`
|
|
12
|
+
allowlist, and `@tanstack/intent` is added as a devDependency so CI can enforce
|
|
13
|
+
skill validity via `intent validate`.
|
|
14
|
+
|
|
15
|
+
## 0.25.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#79](https://github.com/DavideCarvalho/adonis-agora-agent/pull/79) [`88018eb`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/88018ebfeef32be65c464b075d692b62a369d3fd) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix `node ace configure @adonis-agora/agent`, which was broken in every published version.
|
|
20
|
+
|
|
21
|
+
The configure codemod runs each stub through Tempura, which compiles the stub **body** into a JavaScript
|
|
22
|
+
template literal. A bare backtick in the body closes that literal; a bare `${` opens an interpolation.
|
|
23
|
+
All four published stubs were full of both, in ordinary JSDoc prose — `` `lucid` ``, `` `model` ``,
|
|
24
|
+
`` `${documentId}#<n>` `` — so every one of them threw at render time.
|
|
25
|
+
|
|
26
|
+
`configure` therefore aborted on the very first stub, _after_ `updateRcFile` had already succeeded. The
|
|
27
|
+
result was worse than nothing happening: `adonisrc.ts` came out referencing the agent and dashboard
|
|
28
|
+
providers while `config/agent.ts` was never written, leaving an app that could not boot.
|
|
29
|
+
|
|
30
|
+
Both constructs are now backslash-escaped in the stub bodies (`` \` ``, `\${`). This is a render-time
|
|
31
|
+
concern only — the generated files are **byte-identical** to what the stubs always intended, backticked
|
|
32
|
+
prose and all, which was verified against the pre-fix sources.
|
|
33
|
+
|
|
34
|
+
The reason no gate caught this: every stub harness here rendered by stripping the `{{{ }}}` header with a
|
|
35
|
+
regex and using the remainder, which is not what the generator does. A gate that renders differently from
|
|
36
|
+
the generator is not testing the generator. All of them now render through the real engine
|
|
37
|
+
(`app.stubs.create().build().prepare()`), and a new `stub-render.spec.ts` asserts that every stub
|
|
38
|
+
`configure.ts` publishes actually renders — including the two config stubs, which no test had touched.
|
|
39
|
+
|
|
40
|
+
Users on an earlier version who ran `configure` and saw it fail should re-run it after upgrading; if
|
|
41
|
+
`adonisrc.ts` already lists the providers, the codemod is idempotent and only the missing files are written.
|
|
42
|
+
|
|
3
43
|
## 0.25.1
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -7,17 +7,17 @@ import { defineConfig, stores, AuthActorResolver } from '@adonis-agora/agent'
|
|
|
7
7
|
// import { openai } from '@ai-sdk/openai'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Configuration for @adonis-agora/agent. Only
|
|
11
|
-
*
|
|
12
|
-
* in-memory store. The runner is in-process by default (
|
|
10
|
+
* Configuration for @adonis-agora/agent. Only \`model\` is required. Pick a \`store\` by name from the
|
|
11
|
+
* \`stores\` map (each driver's peer is imported lazily, only when selected); omit \`store\` for the
|
|
12
|
+
* in-memory store. The runner is in-process by default (\`durable\` is deferred). The actor resolver
|
|
13
13
|
* defaults to one that THROWS — the agent never fabricates a caller — so wire a real one.
|
|
14
14
|
*
|
|
15
|
-
* Publish the tables with
|
|
15
|
+
* Publish the tables with \`node ace migration:run\` after configuring.
|
|
16
16
|
*/
|
|
17
17
|
export default defineConfig({
|
|
18
18
|
/**
|
|
19
|
-
* The LLM provider. Wrap the Vercel AI SDK via
|
|
20
|
-
* or supply any
|
|
19
|
+
* The LLM provider. Wrap the Vercel AI SDK via \`aiSdkModel(...)\` (from \`@adonis-agora/agent/ai-sdk\`)
|
|
20
|
+
* or supply any \`ModelProvider\`. A lazy thunk keeps the provider SDK peer imported only at boot:
|
|
21
21
|
*
|
|
22
22
|
* model: () => aiSdkModel({ model: openai('gpt-4o-mini') }),
|
|
23
23
|
*/
|
|
@@ -40,7 +40,7 @@ export default defineConfig({
|
|
|
40
40
|
// maxSteps: 8, // cap on model↔tool iterations per turn
|
|
41
41
|
|
|
42
42
|
// Governance console — served automatically at <path>/dashboard (default /agent/dashboard), no
|
|
43
|
-
// separate install. It only mounts once
|
|
43
|
+
// separate install. It only mounts once \`governanceAuthorize\` is set (every panel but Quota reads
|
|
44
44
|
// the cross-actor governance routes, which need that gate too — see the docs). Toggle it off,
|
|
45
45
|
// relocate it, or add an extra gate on top of governanceAuthorize:
|
|
46
46
|
// dashboard: { enabled: true, path: '/agent/dashboard', authorize: (actor) => actor.roles?.includes('ADMIN') ?? false },
|
|
@@ -48,7 +48,7 @@ export default defineConfig({
|
|
|
48
48
|
|
|
49
49
|
// Multimodal uploads — mounts POST /agent/attachments, which stages a file into a model-fetchable
|
|
50
50
|
// MessageAttachment the client sends back with its next chat message. Bind your own store (presign
|
|
51
|
-
// S3/GCS or wrap your media pipeline);
|
|
51
|
+
// S3/GCS or wrap your media pipeline); \`attachmentStores.memory()\` inlines bytes as a data: URL.
|
|
52
52
|
// attachmentStaging: attachmentStores.memory(),
|
|
53
53
|
// attachmentMaxBytes: 20 * 1024 * 1024,
|
|
54
54
|
// attachmentAllowedContentTypes: ['image/png', 'image/jpeg', 'application/pdf'],
|
|
@@ -6,10 +6,10 @@ import { defineMcpConfig, authKitAuth } from '@adonis-agora/agent/mcp'
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Configuration for the MCP (Model Context Protocol) endpoint. The provider exposes the agent's tool
|
|
9
|
-
* registry over Streamable HTTP:
|
|
10
|
-
*
|
|
9
|
+
* registry over Streamable HTTP: \`tools/list\` mirrors the tools the acting actor may call and
|
|
10
|
+
* \`tools/call\` runs them through the same role-checked registry the agent loop uses.
|
|
11
11
|
*
|
|
12
|
-
* Requires
|
|
12
|
+
* Requires \`@adonis-agora/agent\`'s agent provider (which owns the ToolRegistry singleton) to be
|
|
13
13
|
* registered. Register the MCP provider in adonisrc.ts:
|
|
14
14
|
*
|
|
15
15
|
* providers: [
|
|
@@ -17,8 +17,8 @@ import { defineMcpConfig, authKitAuth } from '@adonis-agora/agent/mcp'
|
|
|
17
17
|
* () => import('@adonis-agora/agent/mcp_provider'),
|
|
18
18
|
* ]
|
|
19
19
|
*
|
|
20
|
-
* The endpoint mounts at
|
|
21
|
-
* the login flow at
|
|
20
|
+
* The endpoint mounts at \`/mcp\`. When \`auth\` exposes OAuth metadata (authKitAuth), MCP clients discover
|
|
21
|
+
* the login flow at \`/.well-known/oauth-protected-resource/mcp\`.
|
|
22
22
|
*/
|
|
23
23
|
export default defineMcpConfig({
|
|
24
24
|
name: 'Agent MCP Server',
|
|
@@ -6,22 +6,22 @@ import db from '@adonisjs/lucid/services/db'
|
|
|
6
6
|
import { PgVectorStore } from '@adonis-agora/agent'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* The pgvector chunk table for the @adonis-agora/agent
|
|
10
|
-
* embedded chunk (id
|
|
11
|
-
* metadata column filtered with
|
|
9
|
+
* The pgvector chunk table for the @adonis-agora/agent \`retrievers.pgvector({...})\` store: one row per
|
|
10
|
+
* embedded chunk (id \`\${documentId}#<n>\`), the chunk text, an optional citation source, a \`jsonb\`
|
|
11
|
+
* metadata column filtered with \`@>\`, and the \`vector(N)\` embedding ranked with the \`<=>\`/\`<->\`/\`<#>\`
|
|
12
12
|
* operators. Requires the pgvector extension — Postgres only. Delete this file if you do not use
|
|
13
13
|
* pgvector RAG.
|
|
14
14
|
*
|
|
15
|
-
* Like the agent tables migration, this DELEGATES rather than copying DDL:
|
|
15
|
+
* Like the agent tables migration, this DELEGATES rather than copying DDL: \`PgVectorStore\` owns the
|
|
16
16
|
* column names and the index operator class its own queries depend on, and it exposes the same
|
|
17
|
-
* provisioning the store runs for itself under
|
|
18
|
-
*
|
|
17
|
+
* provisioning the store runs for itself under \`ensureSchema: true\`. Every statement is
|
|
18
|
+
* \`IF NOT EXISTS\`, so this is safe against a database that already has the table.
|
|
19
19
|
*
|
|
20
|
-
* SET THESE to match your retriever config in
|
|
20
|
+
* SET THESE to match your retriever config in \`config/agent.ts\` — they must agree, or the store will
|
|
21
21
|
* query a table shaped differently from the one this created:
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
22
|
+
* - \`DIMENSION\` — your embedding model's width (1536 for text-embedding-3-small).
|
|
23
|
+
* - \`METRIC\` — 'cosine' (default), 'l2', or 'inner'. Picks the index operator class.
|
|
24
|
+
* - \`TABLE\` — only if you overrode \`table\` on the retriever.
|
|
25
25
|
*/
|
|
26
26
|
const TABLE = 'agent_rag_chunks'
|
|
27
27
|
const DIMENSION = 1536
|
|
@@ -6,36 +6,36 @@ import db from '@adonisjs/lucid/services/db'
|
|
|
6
6
|
import { createAgentTables, dropAgentTables } from '@adonis-agora/agent'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* The six tables for the @adonis-agora/agent
|
|
9
|
+
* The six tables for the @adonis-agora/agent \`lucid\` store: threads, messages, tool calls, token
|
|
10
10
|
* usage, model pricing, and the run lifecycle — plus their indexes.
|
|
11
11
|
*
|
|
12
12
|
* This migration DELEGATES to the library instead of reproducing its DDL. The schema belongs to the
|
|
13
|
-
* library:
|
|
13
|
+
* library: \`LucidAgentStore\`, the pricing store and the governance read-model all decide together what
|
|
14
14
|
* columns they read and write, so a hand-copied snapshot here can only ever be right until the next
|
|
15
|
-
* release. It was already wrong in two ways. It reproduced
|
|
15
|
+
* release. It was already wrong in two ways. It reproduced \`createTable(...)\` with no existence guard,
|
|
16
16
|
* which THROWS on a database where the tables are already there — and they usually are, because
|
|
17
|
-
*
|
|
17
|
+
* \`autoCreateTables\` defaults to \`true\` and any of those three stores provisions the shared schema on
|
|
18
18
|
* its first use. And it took two files to say what the library says in one: a second additive migration
|
|
19
|
-
* existed purely to ALTER in the
|
|
19
|
+
* existed purely to ALTER in the \`run_id\` columns this one had been written before.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
* is missing and ALTERs the
|
|
21
|
+
* \`createAgentTables\` is idempotent (\`CREATE TABLE IF NOT EXISTS\`) and additive-only: it creates what
|
|
22
|
+
* is missing and ALTERs the \`run_id\` columns into a database that predates run tracking. So it is safe
|
|
23
23
|
* against an empty database, against one the library already auto-created, and against one left over
|
|
24
24
|
* from an older version — which is exactly the set of states a real app is in.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* \`db\` (the \`Database\` manager from \`@adonisjs/lucid/services/db\`) rather than \`this.db\`: the manager
|
|
27
27
|
* checks out its own connection from the pool, which is what keeps the DDL out of the migrator's
|
|
28
|
-
* transaction (see
|
|
29
|
-
*
|
|
28
|
+
* transaction (see \`disableTransactions\` below). \`this.db\` still supplies the connection NAME, so
|
|
29
|
+
* \`node ace migration:run --connection=x\` provisions the tables on connection \`x\` and not on the default.
|
|
30
30
|
*/
|
|
31
31
|
export default class extends BaseSchema {
|
|
32
32
|
/**
|
|
33
|
-
* REQUIRED, not a preference. The DDL below runs on a connection the
|
|
33
|
+
* REQUIRED, not a preference. The DDL below runs on a connection the \`Database\` manager checks out
|
|
34
34
|
* itself — it is not part of the transaction the migrator would otherwise open for this migration.
|
|
35
|
-
* With
|
|
36
|
-
* connection while
|
|
35
|
+
* With \`pool: { max: 1 }\` (Adonis's own SQLite guidance) that transaction would hold the only
|
|
36
|
+
* connection while \`createAgentTables\` waited for a free one, and the migration would hang until the
|
|
37
37
|
* acquire timeout. Nothing is lost by opting out: the statements were never inside that transaction
|
|
38
|
-
* anyway, and re-running an interrupted
|
|
38
|
+
* anyway, and re-running an interrupted \`createAgentTables\` is a no-op.
|
|
39
39
|
*/
|
|
40
40
|
static disableTransactions = true
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/agent",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
4
4
|
"description": "Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -112,7 +112,8 @@
|
|
|
112
112
|
"files": [
|
|
113
113
|
"dist/",
|
|
114
114
|
"README.md",
|
|
115
|
-
"CHANGELOG.md"
|
|
115
|
+
"CHANGELOG.md",
|
|
116
|
+
"skills"
|
|
116
117
|
],
|
|
117
118
|
"sideEffects": false,
|
|
118
119
|
"dependencies": {
|
|
@@ -184,6 +185,7 @@
|
|
|
184
185
|
"@adonisjs/core": "7.4.0",
|
|
185
186
|
"@adonisjs/lucid": "22.4.2",
|
|
186
187
|
"@qdrant/js-client-rest": "1.19.0",
|
|
188
|
+
"@tanstack/intent": "^0.3.2",
|
|
187
189
|
"@types/node": "22.20.1",
|
|
188
190
|
"@types/react": "19.2.18",
|
|
189
191
|
"ai": "7.0.59",
|
|
@@ -208,6 +210,7 @@
|
|
|
208
210
|
"governance",
|
|
209
211
|
"ai-sdk",
|
|
210
212
|
"durable",
|
|
213
|
+
"tanstack-intent",
|
|
211
214
|
"agora"
|
|
212
215
|
],
|
|
213
216
|
"scripts": {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-governance
|
|
3
|
+
description: >-
|
|
4
|
+
Lock down @adonis-agora/agent: the ActorResolver identity seam (AuthActorResolver over
|
|
5
|
+
ctx.auth.user with toActor mapper, HeaderActorResolver x-actor-id/x-actor-role behind a
|
|
6
|
+
gateway, UnconfiguredActorResolver throwing default), DefaultToolAuthorizer role
|
|
7
|
+
intersection with the offered-tools + invoke-time double check, object-level ownership
|
|
8
|
+
via evaluateOwnership (404 for unknown ids), governanceAuthorize mounting semantics for
|
|
9
|
+
/agent/governance/* (404 without it) and the dashboard mount refusals,
|
|
10
|
+
evaluateGovernanceGate fail-closed behavior, and GET /agent/approvals/mine exemption.
|
|
11
|
+
Use for "tool forbidden", "governance routes 404", "dashboard won't mount", "IDOR /
|
|
12
|
+
cross-actor access", "swap roles for ability checks".
|
|
13
|
+
metadata:
|
|
14
|
+
type: core
|
|
15
|
+
library: "@adonis-agora/agent"
|
|
16
|
+
library_version: "0.25.2"
|
|
17
|
+
framework: adonisjs
|
|
18
|
+
sources:
|
|
19
|
+
- "DavideCarvalho/adonis-agent:packages/adonis/docs/governance/authorization.mdx"
|
|
20
|
+
- "DavideCarvalho/adonis-agent:packages/adonis/docs/governance/dashboard.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-agent:packages/adonis/src/governance-gate.ts"
|
|
22
|
+
- "DavideCarvalho/adonis-agent:packages/adonis/src/ownership.ts"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Governance: identity, authorization, ownership
|
|
26
|
+
|
|
27
|
+
Authorization in `@adonis-agora/agent` is fail-closed at three layers: **who is calling**
|
|
28
|
+
(an `ActorResolver` that never fabricates an identity), **what they may do** (a
|
|
29
|
+
`RolesPolicy` checked twice — before the model sees a tool and again at invoke time),
|
|
30
|
+
and **which records they may address** (object-level ownership on every run/thread route).
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Wire identity once; everything downstream keys off it:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// config/agent.ts
|
|
38
|
+
import { defineConfig, AuthActorResolver } from '@adonis-agora/agent'
|
|
39
|
+
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
model: () => aiSdkModel(openai('gpt-4o-mini')),
|
|
42
|
+
// Reads ctx.auth.user (populated by @adonisjs/auth). Throws when unauthenticated —
|
|
43
|
+
// no identity is ever invented. Map non-standard user shapes with toActor.
|
|
44
|
+
actorResolver: new AuthActorResolver({
|
|
45
|
+
toActor: (user) => ({
|
|
46
|
+
id: String(user.id),
|
|
47
|
+
roles: user.roleNames, // e.g. pulled from a relation
|
|
48
|
+
tenantRef: user.tenantId,
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
defaultRoles: ['STAFF'], // roles a tool requires when it declares none
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Core patterns
|
|
56
|
+
|
|
57
|
+
### Pattern 1 — swap role checks for ability checks
|
|
58
|
+
|
|
59
|
+
`roles` and `ability` share one seam. The default policy intersects roles; an
|
|
60
|
+
ability-aware policy (the `@adonis-agora/authz` Bouncer adapter, `authzToolAuthorizer`)
|
|
61
|
+
consults each tool's `ability` instead — tenant-scoped and fail-closed. Swap only the
|
|
62
|
+
binding; tools keep both annotations:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// config/agent.ts
|
|
66
|
+
import { defineConfig } from '@adonis-agora/agent'
|
|
67
|
+
import { authzToolAuthorizer } from '@adonis-agora/agent/authz'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
// ...
|
|
71
|
+
authorizer: authzToolAuthorizer, // alias: rolesPolicy — one seam, both filter layers follow
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Because the offered-tools filter AND the invoke-time re-check call the same
|
|
76
|
+
`can(actor, tool)`, swapping the policy changes both layers together — they can never
|
|
77
|
+
disagree about what an actor may reach.
|
|
78
|
+
|
|
79
|
+
Source: `packages/adonis/docs/governance/authorization.mdx` ("Ability-aware
|
|
80
|
+
authorization"), `packages/adonis/src/authorizer.ts`.
|
|
81
|
+
|
|
82
|
+
### Pattern 2 — mount the governance read-model with `governanceAuthorize`
|
|
83
|
+
|
|
84
|
+
The cross-actor `/agent/governance/*` read routes exist ONLY when a gate is configured.
|
|
85
|
+
Omit it and every one of those paths answers 404 — by design, so an ungated read-model of
|
|
86
|
+
every actor's spend/threads/approvals can never be served by accident:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// config/agent.ts — typically an ADMIN check; runs AFTER the actor resolves.
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
// ...
|
|
92
|
+
governanceAuthorize: (actor, _ctx) => actor.roles?.includes('ADMIN') ?? false,
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`evaluateGovernanceGate` applies it fail-closed: falsy → 403, thrown → 403 (message shown
|
|
97
|
+
only outside production). The SAME predicate feeds object-level ownership's `privileged`
|
|
98
|
+
flag, and the dashboard console refuses to mount without it.
|
|
99
|
+
|
|
100
|
+
Source: `packages/adonis/docs/governance/authorization.mdx` ("Governance route
|
|
101
|
+
authorization"), `packages/adonis/src/governance-gate.ts`.
|
|
102
|
+
|
|
103
|
+
### Pattern 3 — unit-test your gates router-free
|
|
104
|
+
|
|
105
|
+
Both decision cores are exported pure functions, so you can test them without booting an
|
|
106
|
+
app:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { evaluateOwnership, evaluateGovernanceGate } from '@adonis-agora/agent'
|
|
110
|
+
|
|
111
|
+
evaluateOwnership('u_1', 'u_1', false) // → { ok: true, status: 200 } owner
|
|
112
|
+
evaluateOwnership('u_2', 'u_1', false) // → { ok: false, status: 403 } not owner
|
|
113
|
+
evaluateOwnership('u_2', null, false) // → { ok: false, status: 404 } unknown id
|
|
114
|
+
evaluateOwnership('u_2', 'u_1', true) // → { ok: true, status: 200 } privileged
|
|
115
|
+
|
|
116
|
+
const verdict = await evaluateGovernanceGate(actor, ctx, config.governanceAuthorize, !app.inProduction)
|
|
117
|
+
if (!verdict.ok) return ctx.response.status(verdict.status).json({ error: verdict.error })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Unknown ids deliberately reply 404, never 403, so a response never confirms to a caller
|
|
121
|
+
that an id they don't own exists.
|
|
122
|
+
|
|
123
|
+
Source: `packages/adonis/docs/governance/authorization.mdx` ("Object-level ownership"),
|
|
124
|
+
`packages/adonis/src/ownership.ts`.
|
|
125
|
+
|
|
126
|
+
## Common mistakes
|
|
127
|
+
|
|
128
|
+
### CRITICAL — assuming `HeaderActorResolver` is production-safe
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// Wrong — anyone can send x-actor-id: u_admin and become admin unless a gateway strips these.
|
|
132
|
+
actorResolver: new HeaderActorResolver()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// Correct — read a VERIFIED principal in production; headers are for dev/gateway use only.
|
|
137
|
+
actorResolver: new AuthActorResolver()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Mechanism: `HeaderActorResolver` trusts client-sent `x-actor-id` / `x-actor-role` /
|
|
141
|
+
`x-tenant-ref`; it is safe only behind a gateway that strips and re-sets them from an
|
|
142
|
+
authenticated principal. It does throw when `x-actor-id` is missing, but any caller who
|
|
143
|
+
sets the header IS that actor.
|
|
144
|
+
Source: `packages/adonis/src/actor-resolver.ts` (`HeaderActorResolver` security note),
|
|
145
|
+
`packages/adonis/docs/governance/authorization.mdx`.
|
|
146
|
+
|
|
147
|
+
### HIGH — expecting `/agent/governance/*` to exist after configuring only `governanceQueries`
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// Wrong — read-model resolves, but no gate → the 11 governance routes are NEVER mounted (all 404).
|
|
151
|
+
export default defineConfig({ store: 'lucid', stores: { lucid: stores.lucid() } })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// Correct — set the gate to mount them (gated); or explicitly opt into open reads.
|
|
156
|
+
export default defineConfig({
|
|
157
|
+
...,
|
|
158
|
+
governanceAuthorize: (actor) => actor.roles?.includes('ADMIN') ?? false,
|
|
159
|
+
// governanceAuthorize: () => true, // deliberate legacy behaviour: ANY authenticated actor reads
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Mechanism: mounting is conditional on the gate because the data is cross-actor; a boot
|
|
164
|
+
warning names both ways forward when the read-model resolved without a gate. Note
|
|
165
|
+
`GET /agent/approvals/mine` is exempt — always mounted when the read-model resolves, and
|
|
166
|
+
always scoped to the calling actor.
|
|
167
|
+
Source: `packages/adonis/src/define_config.ts` (`AgentConfig.governanceAuthorize`),
|
|
168
|
+
`packages/adonis/docs/streaming-and-http.mdx` ("Five optional surfaces").
|
|
169
|
+
|
|
170
|
+
### HIGH — treating `dashboard.authorize` as the dashboard's data gate
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// Wrong — authorize gates only the SPA shell; panels still 403/404 without governanceAuthorize.
|
|
174
|
+
dashboard: { enabled: true, authorize: (actor) => actor.roles?.includes('ADMIN') ?? false }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Correct — one predicate shape on BOTH knobs: shell gate + data gate.
|
|
179
|
+
governanceAuthorize: (actor) => actor.roles?.includes('ADMIN') ?? false,
|
|
180
|
+
dashboard: { enabled: true, authorize: (actor) => actor.roles?.includes('ADMIN') ?? false },
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Mechanism: the console is a pure consumer of `/agent/governance/*`; `authorize` decides
|
|
184
|
+
who may load the shell while `governanceAuthorize` decides whether the console mounts at
|
|
185
|
+
all and whether its data routes answer. With no gate the provider logs a boot warning and
|
|
186
|
+
refuses to mount rather than serve seven dead panels.
|
|
187
|
+
Source: `packages/adonis/src/dashboard/define_config.ts` (`decideDashboardMount`,
|
|
188
|
+
`NO_GOVERNANCE_GATE_WARNING`).
|
|
189
|
+
|
|
190
|
+
### MEDIUM — giving a per-agent resolver a different identity than the global one
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Wrong — webhook agents resolve actor 'body:svc-1'; the global resolver would say 'user_9'.
|
|
194
|
+
agents: [{ name: 'webhook', actorResolver: new BodyActorResolver() }]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// Correct — a per-agent resolver must yield an id the GLOBAL resolver could also produce.
|
|
199
|
+
agents: [
|
|
200
|
+
{
|
|
201
|
+
name: 'webhook',
|
|
202
|
+
actorResolver: new AuthActorResolver({ toActor: mapServicePrincipal }), // same id space
|
|
203
|
+
},
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Mechanism: the override applies ONLY to `POST /agent/chat`; stream re-attach, cancel,
|
|
208
|
+
approve/reject, threads, quota, and all governance routes resolve the GLOBAL resolver —
|
|
209
|
+
a mismatched `actor.id` means the caller starts a run it cannot stream, cancel, or
|
|
210
|
+
approve (ownership compares ids).
|
|
211
|
+
Source: `packages/adonis/docs/authoring/personas-and-agents.mdx` ("The override applies
|
|
212
|
+
to POST /agent/chat only").
|
|
213
|
+
|
|
214
|
+
### MEDIUM — designing a client around a thrown gate's message
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// Wrong — reading error.message from a 403 to branch UI logic; prod never carries it.
|
|
218
|
+
if (err.message.includes('missing tenant')) showTenantPicker()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// Correct — treat 403 as opaque; debug details exist only outside production.
|
|
223
|
+
if (err.status === 403) showForbidden() // body is exactly { "error": "forbidden" } in prod
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Mechanism: `evaluateGovernanceGate` passes the thrown message through only when `debug`
|
|
227
|
+
is true (the provider passes `!app.inProduction`); in production the body is the generic
|
|
228
|
+
string, because authorization predicates routinely throw with sensitive detail.
|
|
229
|
+
Source: `packages/adonis/src/governance-gate.ts`, `packages/adonis/docs/governance/authorization.mdx`
|
|
230
|
+
("A thrown message never reaches production clients").
|
|
231
|
+
|
|
232
|
+
See also: `agent-setup/SKILL.md` — wiring `AuthActorResolver` at setup time;
|
|
233
|
+
`agent-console-client/SKILL.md` — how the console consumes these gated routes.
|