@segmentstream-cloud/ontology 0.14.0 → 0.15.0
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/README.md +35 -10
- package/dist/agent-chat.d.ts +75 -0
- package/dist/agent-chat.js +176 -0
- package/dist/analyze.d.ts +6 -2
- package/dist/analyze.js +29 -18
- package/dist/authoring.d.ts +6 -3
- package/dist/authoring.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/permissions.d.ts +11 -2
- package/dist/permissions.js +15 -3
- package/dist/replication/baseline.js +1 -1
- package/dist/replication/model.js +2 -2
- package/dist/replication/schema.js +1 -1
- package/dist/runtime/agents.d.ts +1 -0
- package/dist/runtime/agents.js +160 -0
- package/dist/runtime/generated-actions.d.ts +2 -1
- package/dist/runtime/generated-actions.js +18 -13
- package/dist/service/agent/channel.d.ts +16 -0
- package/dist/service/agent/channel.js +88 -0
- package/dist/service/agent/protocol.d.ts +14 -0
- package/dist/service/agent/protocol.js +26 -0
- package/dist/service/agent/server.d.ts +2 -1
- package/dist/service/agent/server.js +24 -2
- package/dist/service/aggregate-platform.d.ts +5 -0
- package/dist/service/aggregate-platform.js +1 -1
- package/dist/service/config.d.ts +10 -0
- package/dist/service/config.js +8 -0
- package/dist/service/handlers.d.ts +2 -1
- package/dist/service/handlers.js +2 -2
- package/dist/service/mcp/server.d.ts +3 -0
- package/dist/service/mcp/server.js +1 -1
- package/dist/service/operations.d.ts +6 -2
- package/dist/service/operations.js +13 -5
- package/dist/service/runtime-contract.d.ts +26 -0
- package/dist/service/service.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,7 +143,8 @@ export default defineOntology({
|
|
|
143
143
|
- `rel.one` and `rel.many` declare relationships. Between stored entities, `rel.one` adds a
|
|
144
144
|
key and `rel.many` a join table. With a warehouse-backed end, `rel.one` declares one `on`
|
|
145
145
|
pair and is matched by value. `User` is the built-in entity for app users and service
|
|
146
|
-
principals
|
|
146
|
+
principals; `AgentChat` (`agent_chat`) is the built-in entity for the group chats of the
|
|
147
|
+
app's agent (see [Agents](#agents)). Every ontology includes both.
|
|
147
148
|
- `role(name, { description, permissions })` declares a role: a named set of permissions.
|
|
148
149
|
`member` is built in: every active app user holds it, with the permissions of
|
|
149
150
|
`defineOntology({ member })`.
|
|
@@ -162,7 +163,9 @@ export default defineOntology({
|
|
|
162
163
|
|
|
163
164
|
A permission is `<entity>:<verb>`. The verbs are `read`, the generated `create`, `update`,
|
|
164
165
|
`delete`, `link` and `unlink`, and the names of authored actions. A role may use `*` for the
|
|
165
|
-
entity, the verb or both: `lead:*`, `*:read`, `*:*`.
|
|
166
|
+
entity, the verb or both: `lead:*`, `*:read`, `*:*`. An entity wildcard never covers the
|
|
167
|
+
built-in `agent_chat`: reading or changing it needs a permission that names it, such as
|
|
168
|
+
`agent_chat:read`.
|
|
166
169
|
|
|
167
170
|
- Reads require `<entity>:read`: lists, queries, records, history, both entities of a
|
|
168
171
|
relationship traversal, and every entity an aggregate touches. `include` and a filter through a relationship require `read` on
|
|
@@ -216,6 +219,7 @@ one is missing or invalid, and never repeats the database URL.
|
|
|
216
219
|
| `SEGMENTSTREAM_APP_CREDENTIAL` | Optional. The workspace app credential; without it, aggregates answer 503. |
|
|
217
220
|
| `SEGMENTSTREAM_RUNTIME_OIDC_ISSUER` | Optional. The team's Vercel OIDC issuer, for the app's agent. |
|
|
218
221
|
| `SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE` | Optional, set with the issuer. The audience of the team's tokens. |
|
|
222
|
+
| `VERCEL_PROJECT_PRODUCTION_URL` | Set by Vercel. With the runtime pair, where `agent_chat.send_message` reaches the agent. |
|
|
219
223
|
|
|
220
224
|
The platform sets these variables in the workspace deployment, the Neon Auth pair once the
|
|
221
225
|
workspace has Neon Auth and the runtime pair on deployments with an agent. With the runtime
|
|
@@ -268,14 +272,15 @@ The agent acts for a member by adding `X-SegmentStream-Act-As: <user id>` on any
|
|
|
268
272
|
member, as if they had called. A member who is inactive or has no live pairing is a 403.
|
|
269
273
|
Without the header the agent may call only the agent routes:
|
|
270
274
|
|
|
271
|
-
| Route | Caller | Does
|
|
272
|
-
| ------------------------------------------------------------- | ------ |
|
|
273
|
-
| `POST /api/ontology/agent/pairings` | agent | `{ provider, external_id, display? }` → `{ code, expires_at }`
|
|
274
|
-
| `GET /api/ontology/agent/pairings/{code}` | member | `{ provider, display, expires_at }`, for the connect page
|
|
275
|
-
| `POST /api/ontology/agent/pairings/{code}/approve` | member | Pairs the account with the member → `{ identity }`
|
|
276
|
-
| `GET /api/ontology/agent/identities/{provider}/{external_id}` | agent | `{ user: { id, name }, roles }`; 404 unpaired, 403 revoked, inactive
|
|
277
|
-
| `GET /api/ontology/agent/identities` | member | `{ identities }`: the member's live identities
|
|
278
|
-
| `DELETE /api/ontology/agent/identities/{id}` | member | Revokes one of them: 204
|
|
275
|
+
| Route | Caller | Does |
|
|
276
|
+
| ------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `POST /api/ontology/agent/pairings` | agent | `{ provider, external_id, display? }` → `{ code, expires_at }` |
|
|
278
|
+
| `GET /api/ontology/agent/pairings/{code}` | member | `{ provider, display, expires_at }`, for the connect page |
|
|
279
|
+
| `POST /api/ontology/agent/pairings/{code}/approve` | member | Pairs the account with the member → `{ identity }` |
|
|
280
|
+
| `GET /api/ontology/agent/identities/{provider}/{external_id}` | agent | `{ user: { id, name }, roles }`; 404 unpaired, 403 revoked, inactive |
|
|
281
|
+
| `GET /api/ontology/agent/identities` | member | `{ identities }`: the member's live identities |
|
|
282
|
+
| `DELETE /api/ontology/agent/identities/{id}` | member | Revokes one of them: 204 |
|
|
283
|
+
| `POST /api/ontology/agent/chats/seen` | agent | `{ provider, externalId, title, memberId?, leftMemberId?, agentLeft?, migratedFrom? }` → `{ id, title, purpose, whenToUse }` |
|
|
279
284
|
|
|
280
285
|
A pairing code is eight characters without look-alikes (no 0, O, 1, I or L), stored as its
|
|
281
286
|
SHA-256, valid ten minutes and used once. Each instance allows a client address 60 new
|
|
@@ -286,6 +291,26 @@ agent, even acting as a member, so only members pair. Pairings and identities li
|
|
|
286
291
|
runtime tables `_agent_pairings` and `_agent_identities`; the migration creates them in a
|
|
287
292
|
database an earlier SDK migrated.
|
|
288
293
|
|
|
294
|
+
The built-in entity `agent_chat` keeps the group chats the agent is in. The agent reports each
|
|
295
|
+
message it sees there to `chats/seen`, which creates the chat or keeps its title current,
|
|
296
|
+
records `lastMessageAt` at most every 15 minutes, links a paired, active writer to
|
|
297
|
+
`members` and unlinks `leftMemberId`, a paired member who left, when linked, or every member
|
|
298
|
+
with `agentLeft: true`, when the agent itself left (the chat stays). With `migratedFrom`, the
|
|
299
|
+
old id of a group that became a supergroup, it moves the old record to `externalId`, or, when
|
|
300
|
+
both are recorded, moves the members and a missing `purpose` and `whenToUse` to the new one and
|
|
301
|
+
deletes the old one. `provider`, `externalId`, `title` and `lastMessageAt` are
|
|
302
|
+
managed. Members set `purpose` and `whenToUse` with the generated
|
|
303
|
+
`agent_chat.update`, the chat's only generated action. The built-in action
|
|
304
|
+
`agent_chat.send_message` (`{ message, chat?, person? }`, `message` at most 4000 characters) asks the agent to write to the chat
|
|
305
|
+
given, else to the person's one chat, else to their paired private chat; a person in several
|
|
306
|
+
chats is refused with the candidates, or only their number without `agent_chat:read`. The service posts the message to the agent at
|
|
307
|
+
`https://<VERCEL_PROJECT_PRODUCTION_URL>/eve/v1/telegram/messages` with the deployment's OIDC
|
|
308
|
+
token, naming the requester with their `id`, `name`, `kind`, `roles` and `actAs`, true for a
|
|
309
|
+
member with a live pairing, whom the agent's turn can act as; without the runtime
|
|
310
|
+
pair, or when the agent refuses, the action is rejected with the reason. Both actions need their own permission (`agent_chat:update`,
|
|
311
|
+
`agent_chat:send_message`), which no role holds until the workspace grants it, and reading
|
|
312
|
+
chats needs `agent_chat:read` by name: `*:read` and `*:*` do not cover `agent_chat`.
|
|
313
|
+
|
|
289
314
|
## The `segmentstream-ontology` binary
|
|
290
315
|
|
|
291
316
|
Run the binary in the `ontology/` folder. The entry defaults to `./index.ts`. The binary
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Context, Effect, Schema } from "effect";
|
|
2
|
+
import { ActionRejected } from "./errors.js";
|
|
3
|
+
/** The only channel the agent delivers to so far, and the provider of its chats. */
|
|
4
|
+
export declare const TELEGRAM = "telegram";
|
|
5
|
+
/**
|
|
6
|
+
* What `agent_chat.send_message` asks the agent to deliver: the body of the agent's
|
|
7
|
+
* `POST /eve/v1/telegram/messages`. A Telegram private chat's id is the person's Telegram user id.
|
|
8
|
+
*/
|
|
9
|
+
export interface AgentMessage {
|
|
10
|
+
readonly target: {
|
|
11
|
+
readonly kind: "group" | "private";
|
|
12
|
+
readonly provider: typeof TELEGRAM;
|
|
13
|
+
readonly chatId: string;
|
|
14
|
+
};
|
|
15
|
+
/** The request for the agent, written as an instruction to it. */
|
|
16
|
+
readonly message: string;
|
|
17
|
+
/** Whom the agent should address, when the caller named someone. */
|
|
18
|
+
readonly person: {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
} | null;
|
|
22
|
+
/** Who asked: the principal that ran the action. */
|
|
23
|
+
readonly requester: {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly kind: "member" | "service";
|
|
27
|
+
/** The role names the principal holds, which pick the model of a session the request starts. */
|
|
28
|
+
readonly roles: ReadonlyArray<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the agent can act as the requester in the turn: a member with a live paired identity.
|
|
31
|
+
* Otherwise the turn runs without a member and has no ontology.
|
|
32
|
+
*/
|
|
33
|
+
readonly actAs: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
declare const AgentChannel_base: Context.ServiceClass<AgentChannel, "@segmentstream-cloud/ontology/AgentChannel", {
|
|
37
|
+
/** The Telegram user id of the member's live paired identity: their private chat's id. */
|
|
38
|
+
readonly privateChat: (userId: string) => Effect.Effect<string | undefined>;
|
|
39
|
+
/** Hands the message to the agent; a refusal or failure explains itself to the caller. */
|
|
40
|
+
readonly send: (message: AgentMessage) => Effect.Effect<void, ActionRejected>;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* The workspace's agent, as `agent_chat.send_message` reaches it. The ontology service provides it to
|
|
44
|
+
* actions only on deployments with an agent; without it, the action is refused.
|
|
45
|
+
*/
|
|
46
|
+
export declare class AgentChannel extends AgentChannel_base {
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The built-in agent chat: a group chat the workspace's agent is in. Every ontology includes it,
|
|
50
|
+
* after `User`. The agent records each chat it sees and the members it sees writing there, through
|
|
51
|
+
* `POST /api/ontology/agent/chats/seen`; members say what a chat is for with the generated `update`,
|
|
52
|
+
* which changes only `purpose` and `whenToUse`. `send_message` asks the agent to write to a chat.
|
|
53
|
+
*/
|
|
54
|
+
export declare const AgentChat: import("./entity.js").Entity<"agent_chat", {
|
|
55
|
+
readonly provider: import("./property.js").Property<string, false, false>;
|
|
56
|
+
readonly externalId: import("./property.js").Property<string, false, false>;
|
|
57
|
+
readonly title: import("./property.js").Property<string, false, false>;
|
|
58
|
+
readonly lastMessageAt: Schema.optionalKey<import("./property.js").Property<string, false, false>>;
|
|
59
|
+
readonly purpose: Schema.optionalKey<import("./property.js").Property<string, false, false>>;
|
|
60
|
+
readonly whenToUse: Schema.optionalKey<import("./property.js").Property<string, false, false>>;
|
|
61
|
+
}, {
|
|
62
|
+
readonly members: import("./relationship.js").ManyRelationship;
|
|
63
|
+
}, {
|
|
64
|
+
readonly sendMessage: import("./action.js").Action<Schema.Struct<{
|
|
65
|
+
readonly message: import("./property.js").Property<string, false, false>;
|
|
66
|
+
readonly chat: Schema.optionalKey<Schema.Codec<string, string, never, never>>;
|
|
67
|
+
readonly person: Schema.optionalKey<Schema.Codec<string, string, never, never>>;
|
|
68
|
+
}>, {
|
|
69
|
+
chat: string;
|
|
70
|
+
target: "group" | "private";
|
|
71
|
+
delivered: true;
|
|
72
|
+
}>;
|
|
73
|
+
}, Readonly<Record<never, never>>>;
|
|
74
|
+
export type AgentChat = typeof AgentChat.Type;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Context, Effect, Option, Schema } from "effect";
|
|
2
|
+
import { action } from "./action.js";
|
|
3
|
+
import { entity } from "./entity.js";
|
|
4
|
+
import { ActionRejected } from "./errors.js";
|
|
5
|
+
import { isAllowed, permissionOf, READ_VERB } from "./permissions.js";
|
|
6
|
+
import { p } from "./property.js";
|
|
7
|
+
import { rel } from "./relationship.js";
|
|
8
|
+
import { User } from "./user.js";
|
|
9
|
+
/** The only channel the agent delivers to so far, and the provider of its chats. */
|
|
10
|
+
export const TELEGRAM = "telegram";
|
|
11
|
+
/**
|
|
12
|
+
* The workspace's agent, as `agent_chat.send_message` reaches it. The ontology service provides it to
|
|
13
|
+
* actions only on deployments with an agent; without it, the action is refused.
|
|
14
|
+
*/
|
|
15
|
+
export class AgentChannel extends Context.Service()("@segmentstream-cloud/ontology/AgentChannel") {
|
|
16
|
+
}
|
|
17
|
+
/** A group chat as the agent's target; the agent delivers only to Telegram so far. */
|
|
18
|
+
function groupTarget(chat) {
|
|
19
|
+
return chat.provider === TELEGRAM
|
|
20
|
+
? Effect.succeed({ kind: "group", provider: TELEGRAM, chatId: chat.externalId })
|
|
21
|
+
: Effect.fail(new ActionRejected({
|
|
22
|
+
message: `The agent delivers only to Telegram chats; ${chat.title} is on ${chat.provider}.`,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
const NO_AGENT = "The workspace has no agent deployed, so nobody can deliver the message. " +
|
|
26
|
+
"Deploy the workspace's agent first.";
|
|
27
|
+
/**
|
|
28
|
+
* The built-in agent chat: a group chat the workspace's agent is in. Every ontology includes it,
|
|
29
|
+
* after `User`. The agent records each chat it sees and the members it sees writing there, through
|
|
30
|
+
* `POST /api/ontology/agent/chats/seen`; members say what a chat is for with the generated `update`,
|
|
31
|
+
* which changes only `purpose` and `whenToUse`. `send_message` asks the agent to write to a chat.
|
|
32
|
+
*/
|
|
33
|
+
export const AgentChat = entity("agent_chat", {
|
|
34
|
+
id: "ent_agent_chat",
|
|
35
|
+
label: "Agent chat",
|
|
36
|
+
plural: "Agent chats",
|
|
37
|
+
title: "title",
|
|
38
|
+
description: "A group chat the workspace's agent is in. The agent records each chat it sees; " +
|
|
39
|
+
"members say what the chat is for, so that agents know where to write.",
|
|
40
|
+
properties: {
|
|
41
|
+
provider: p.text({ managed: true, description: "The channel, for example telegram." }),
|
|
42
|
+
externalId: p.text({ managed: true, description: "The chat's id at the provider." }),
|
|
43
|
+
title: p.text({
|
|
44
|
+
managed: true,
|
|
45
|
+
description: "The chat's name at the provider, kept current by the agent.",
|
|
46
|
+
}),
|
|
47
|
+
lastMessageAt: p.timestamp({
|
|
48
|
+
optional: true,
|
|
49
|
+
managed: true,
|
|
50
|
+
label: "Last message",
|
|
51
|
+
description: "When the agent last saw a message in the chat, recorded at most every 15 minutes.",
|
|
52
|
+
}),
|
|
53
|
+
purpose: p.text({
|
|
54
|
+
optional: true,
|
|
55
|
+
maxLength: 64,
|
|
56
|
+
description: "A short key that code and agents look the chat up by, such as accounting.",
|
|
57
|
+
}),
|
|
58
|
+
whenToUse: p.text({
|
|
59
|
+
optional: true,
|
|
60
|
+
maxLength: 1000,
|
|
61
|
+
display: "multiline",
|
|
62
|
+
label: "When to use",
|
|
63
|
+
description: "For agents: what the chat is for and when to write there, such as " +
|
|
64
|
+
'"Invoices and payments; Yulia answers on weekdays."',
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
relationships: {
|
|
68
|
+
// By name, so that an ontology rebuilt with the deployed `user` still resolves it.
|
|
69
|
+
members: rel.many("user", {
|
|
70
|
+
inverse: "agentChats",
|
|
71
|
+
phrase: ["includes", "is in"],
|
|
72
|
+
description: "Workspace members the agent has seen writing in the chat.",
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
indexes: [{ properties: ["provider", "externalId"], unique: true }],
|
|
76
|
+
}).actions((self) => ({
|
|
77
|
+
sendMessage: action({
|
|
78
|
+
description: "Ask the workspace's agent to write to a group chat or a person, for example " +
|
|
79
|
+
'"ask Yulia for the amount on invoice 118". message is the request for the agent, written ' +
|
|
80
|
+
"as an instruction to it, not necessarily the text to post. The target is chosen without " +
|
|
81
|
+
"guessing: with chat, that chat (person, if given, is whom the agent addresses there); with " +
|
|
82
|
+
"only person, the one agent chat whose members include them, or, when they are in none, " +
|
|
83
|
+
"their private chat with the agent if they paired a Telegram account. A person in several " +
|
|
84
|
+
"chats is refused with the candidates (id, title, purpose, when to use; only their number " +
|
|
85
|
+
"without agent_chat:read): call again with chat. The agent then writes in that chat and " +
|
|
86
|
+
"handles the replies there, in that chat's own session; this action only delivers the " +
|
|
87
|
+
"request and does not wait for an answer. Requires an agent deployed in the workspace.",
|
|
88
|
+
input: Schema.Struct({
|
|
89
|
+
message: p.text({
|
|
90
|
+
maxLength: 4000,
|
|
91
|
+
description: "The request for the agent, as an instruction: what to ask or tell, of whom.",
|
|
92
|
+
}),
|
|
93
|
+
chat: p.ref("agent_chat", { optional: true, description: "The agent chat to write in." }),
|
|
94
|
+
person: p.ref("user", {
|
|
95
|
+
optional: true,
|
|
96
|
+
description: "The member to address; alone, it picks their chat with the agent.",
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
hints: { destructive: false },
|
|
100
|
+
connections: [],
|
|
101
|
+
prepare: ({ input, principal }) => Effect.gen(function* () {
|
|
102
|
+
const channel = yield* Effect.serviceOption(AgentChannel);
|
|
103
|
+
if (Option.isNone(channel)) {
|
|
104
|
+
return yield* new ActionRejected({ message: NO_AGENT });
|
|
105
|
+
}
|
|
106
|
+
const person = input.person === undefined ? undefined : yield* User.get(input.person);
|
|
107
|
+
if (person !== undefined && (person.kind !== "member" || !person.active)) {
|
|
108
|
+
return yield* new ActionRejected({
|
|
109
|
+
message: `${person.name} is not an active member, so the agent does not contact them.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
let chat;
|
|
113
|
+
let target;
|
|
114
|
+
if (input.chat !== undefined) {
|
|
115
|
+
chat = yield* self.get(input.chat);
|
|
116
|
+
target = yield* groupTarget(chat);
|
|
117
|
+
}
|
|
118
|
+
else if (person !== undefined) {
|
|
119
|
+
const chats = yield* self.find({
|
|
120
|
+
where: { members: { some: { id: { eq: person.id } } } },
|
|
121
|
+
});
|
|
122
|
+
if (chats.length > 1) {
|
|
123
|
+
// The action reads chats whatever the caller holds; only a reader sees which they are.
|
|
124
|
+
if (!isAllowed(permissionOf(self.name, READ_VERB), principal)) {
|
|
125
|
+
return yield* new ActionRejected({
|
|
126
|
+
message: `${person.name} is in ${chats.length} chats with the agent; call again with ` +
|
|
127
|
+
"the chat's id as chat. Listing them requires agent_chat:read.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const candidates = chats.map((candidate) => `- ${candidate.id}: ${candidate.title}; purpose: ${candidate.purpose ?? "none"}; ` +
|
|
131
|
+
`when to use: ${candidate.whenToUse ?? "not set"}`);
|
|
132
|
+
return yield* new ActionRejected({
|
|
133
|
+
message: `${person.name} is in ${chats.length} chats with the agent; ` +
|
|
134
|
+
`call again with one of them as chat:\n${candidates.join("\n")}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
chat = chats[0];
|
|
138
|
+
if (chat !== undefined) {
|
|
139
|
+
target = yield* groupTarget(chat);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const chatId = yield* channel.value.privateChat(person.id);
|
|
143
|
+
if (chatId === undefined) {
|
|
144
|
+
return yield* new ActionRejected({
|
|
145
|
+
message: `${person.name} is in no chat with the agent and has not paired a Telegram ` +
|
|
146
|
+
"account with it, so the agent cannot reach them.",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
target = { kind: "private", provider: TELEGRAM, chatId };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return yield* new ActionRejected({
|
|
154
|
+
message: "Name the chat to write in, the person to write to, or both.",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const requester = yield* User.get(principal.id);
|
|
158
|
+
// The agent acts as a member only through their pairing.
|
|
159
|
+
const actAs = principal.kind === "member" &&
|
|
160
|
+
(yield* channel.value.privateChat(principal.id)) !== undefined;
|
|
161
|
+
yield* channel.value.send({
|
|
162
|
+
target,
|
|
163
|
+
message: input.message,
|
|
164
|
+
person: person === undefined ? null : { id: person.id, name: person.name },
|
|
165
|
+
requester: {
|
|
166
|
+
id: requester.id,
|
|
167
|
+
name: requester.name,
|
|
168
|
+
kind: principal.kind,
|
|
169
|
+
roles: principal.roles,
|
|
170
|
+
actAs,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return { chat: chat?.id ?? null, target: target.kind, delivered: true };
|
|
174
|
+
}),
|
|
175
|
+
}),
|
|
176
|
+
}));
|
package/dist/analyze.d.ts
CHANGED
|
@@ -9,7 +9,10 @@ import { type Role } from "./role.js";
|
|
|
9
9
|
import { type SignInOptions } from "./sign-in.js";
|
|
10
10
|
/** The input of `defineOntology` and `check`. */
|
|
11
11
|
export interface OntologyConfig<Entities extends ReadonlyArray<AnyEntity> = ReadonlyArray<AnyEntity>> {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The app's entities. The built-in `User` and `AgentChat` are always included; listing them is
|
|
14
|
+
* optional.
|
|
15
|
+
*/
|
|
13
16
|
readonly entities: Entities;
|
|
14
17
|
/** Business roles. The built-in `member` role is always included. */
|
|
15
18
|
readonly roles?: ReadonlyArray<Role>;
|
|
@@ -82,7 +85,8 @@ export interface ResolvedIndex {
|
|
|
82
85
|
}
|
|
83
86
|
/**
|
|
84
87
|
* The permissions of an entity's reads and generated actions. The built-in User has no generated
|
|
85
|
-
* actions
|
|
88
|
+
* actions and the built-in agent chat only `update`; `link` and `unlink` exist when the entity
|
|
89
|
+
* declares a `rel.many`.
|
|
86
90
|
*/
|
|
87
91
|
export interface EntityPermissions {
|
|
88
92
|
readonly read: string;
|
package/dist/analyze.js
CHANGED
|
@@ -7,10 +7,11 @@ import { CONNECTION_KEY, isConnection } from "./connection.js";
|
|
|
7
7
|
import { isEntity } from "./entity.js";
|
|
8
8
|
import { AGGREGATE_ROUTE, LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MCP_READ_TOOLS, MCP_TOOL_NAME_MAX_LENGTH, MEMBER_ROLE, mcpToolName, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
|
|
9
9
|
import { REFERENCE_ANNOTATION, propertyDefinition, referenceTarget, } from "./property.js";
|
|
10
|
-
import { generatedActionPermissions, parsePermission, permissionOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
|
|
10
|
+
import { EXPLICIT_GRANT_ENTITIES, generatedActionPermissions, parsePermission, permissionOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
|
|
11
11
|
import { ON_DELETE, isRelationship } from "./relationship.js";
|
|
12
12
|
import { isRole, member, role } from "./role.js";
|
|
13
13
|
import { DEFAULT_SIGN_IN_POLICY, isSignInPolicy } from "./sign-in.js";
|
|
14
|
+
import { AgentChat } from "./agent-chat.js";
|
|
14
15
|
import { User } from "./user.js";
|
|
15
16
|
import { authoringToWire } from "./authoring.js";
|
|
16
17
|
import { nameToWire } from "./authoring-names.js";
|
|
@@ -34,10 +35,9 @@ export function analyze(config, options = {}) {
|
|
|
34
35
|
diagnostics.push({ severity, code, path, message });
|
|
35
36
|
};
|
|
36
37
|
const original = config;
|
|
37
|
-
const normalized = authoringToWire(config, options.user ?? User, report);
|
|
38
|
+
const normalized = authoringToWire(config, [options.user ?? User, AgentChat], report);
|
|
38
39
|
config = normalized.config;
|
|
39
|
-
const
|
|
40
|
-
const entities = collectEntities(config, user, report);
|
|
40
|
+
const entities = collectEntities(config, normalized.builtIns, report);
|
|
41
41
|
const roles = collectRoles(config, report);
|
|
42
42
|
const signIn = signInOptions(config, report);
|
|
43
43
|
const connections = collectConnections(config, report);
|
|
@@ -49,7 +49,7 @@ export function analyze(config, options = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
const scope = {
|
|
51
51
|
report,
|
|
52
|
-
|
|
52
|
+
builtIns: new Set(normalized.builtIns.map((candidate) => candidate.definition)),
|
|
53
53
|
entitiesByName,
|
|
54
54
|
definitions: new Set(entities.map((candidate) => candidate.definition)),
|
|
55
55
|
entityProperties: new Set([...entities, ...(original.entities ?? []).filter(isEntity)].flatMap((candidate) => Object.values(candidate.definition.properties ?? {}))),
|
|
@@ -74,7 +74,7 @@ export function analyze(config, options = {}) {
|
|
|
74
74
|
function toolOperations(candidate) {
|
|
75
75
|
const { permissions } = candidate;
|
|
76
76
|
const generated = [
|
|
77
|
-
...
|
|
77
|
+
...["create", "update", "delete"].filter((verb) => permissions[verb] !== undefined),
|
|
78
78
|
...(permissions.create !== undefined
|
|
79
79
|
? candidate.properties
|
|
80
80
|
.filter(({ definition }) => definition.unique)
|
|
@@ -169,9 +169,10 @@ function checkConnectionsUsed(entities, connections, declared, report) {
|
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
171
|
* The keys of an action's connections, each listed in `defineOntology({ connections })`, and the
|
|
172
|
-
* phases they require: `prepare` with connections
|
|
172
|
+
* phases they require: `prepare` with connections and only with them, except for built-in actions,
|
|
173
|
+
* and `prepare` or `handler` always.
|
|
173
174
|
*/
|
|
174
|
-
function resolveActionConnections(path, action, scope) {
|
|
175
|
+
function resolveActionConnections(path, action, scope, builtIn) {
|
|
175
176
|
const { report } = scope;
|
|
176
177
|
const declared = action.connections;
|
|
177
178
|
const keys = [];
|
|
@@ -200,7 +201,8 @@ function resolveActionConnections(path, action, scope) {
|
|
|
200
201
|
if (Array.isArray(declared) && declared.length > 0 && !hasPrepare) {
|
|
201
202
|
report("invalid-prepare", `${path}.prepare`, "an action with connections calls them in prepare, which runs before the transaction.");
|
|
202
203
|
}
|
|
203
|
-
|
|
204
|
+
// A built-in action's prepare reaches the platform, such as the workspace's agent, instead.
|
|
205
|
+
if (hasPrepare && !builtIn && Array.isArray(declared) && declared.length === 0) {
|
|
204
206
|
report("invalid-prepare", `${path}.prepare`, "prepare is for calling connections; an action without connections does its work in handler.");
|
|
205
207
|
}
|
|
206
208
|
if (!hasPrepare && !hasHandler) {
|
|
@@ -208,8 +210,8 @@ function resolveActionConnections(path, action, scope) {
|
|
|
208
210
|
}
|
|
209
211
|
return keys;
|
|
210
212
|
}
|
|
211
|
-
function collectEntities(config,
|
|
212
|
-
const entities = [
|
|
213
|
+
function collectEntities(config, builtIns, report) {
|
|
214
|
+
const entities = [...builtIns];
|
|
213
215
|
(config.entities ?? []).forEach((candidate, index) => {
|
|
214
216
|
if (!isEntity(candidate)) {
|
|
215
217
|
report("invalid-entity", `entities[${index}]`, "is not an entity; declare entities with entity(name, { … }).");
|
|
@@ -330,7 +332,7 @@ function resolveEntity(candidate, scope) {
|
|
|
330
332
|
else if (!properties.some((property) => property.name === definition.title)) {
|
|
331
333
|
report("invalid-title", `${name}.title`, `"${definition.title}" is not a declared property of ${name}.`);
|
|
332
334
|
}
|
|
333
|
-
const builtIn =
|
|
335
|
+
const builtIn = scope.builtIns.has(candidate.definition);
|
|
334
336
|
return {
|
|
335
337
|
entity: candidate,
|
|
336
338
|
name,
|
|
@@ -342,11 +344,15 @@ function resolveEntity(candidate, scope) {
|
|
|
342
344
|
permissions: entityPermissions(name, builtIn || definition.source !== undefined, relationships),
|
|
343
345
|
};
|
|
344
346
|
}
|
|
345
|
-
/**
|
|
347
|
+
/**
|
|
348
|
+
* Reads need `<entity>:read`; each generated action needs the permission named after it. Built-in
|
|
349
|
+
* entities hold the platform's records: only `agent_chat` has a generated action, `update`, for the
|
|
350
|
+
* properties members edit.
|
|
351
|
+
*/
|
|
346
352
|
function entityPermissions(name, builtIn, relationships) {
|
|
347
353
|
const read = permissionOf(name, READ_VERB);
|
|
348
354
|
if (builtIn) {
|
|
349
|
-
return { read };
|
|
355
|
+
return name === AgentChat.name ? { read, update: permissionOf(name, "update") } : { read };
|
|
350
356
|
}
|
|
351
357
|
const many = relationships.some(({ relationship }) => relationship.cardinality === "many");
|
|
352
358
|
return {
|
|
@@ -628,7 +634,7 @@ function resolveActions(candidate, scope) {
|
|
|
628
634
|
action,
|
|
629
635
|
permission: resolvePermission(path, permissionOf(candidate.name, name), action, scope),
|
|
630
636
|
input: resolveInput(path, action.input, scope),
|
|
631
|
-
connections: resolveActionConnections(path, action, scope),
|
|
637
|
+
connections: resolveActionConnections(path, action, scope, scope.builtIns.has(candidate.definition)),
|
|
632
638
|
hints: resolveHints(path, action, report),
|
|
633
639
|
});
|
|
634
640
|
}
|
|
@@ -762,7 +768,9 @@ function checkEntityIdentities(entities, report) {
|
|
|
762
768
|
const existingName = names.get(candidate.name);
|
|
763
769
|
const existingId = ids.get(definition.id);
|
|
764
770
|
if (existingName !== undefined) {
|
|
765
|
-
const builtIn = existingName.builtIn
|
|
771
|
+
const builtIn = existingName.builtIn
|
|
772
|
+
? ` (the built-in ${existingName.entity.definition.label})`
|
|
773
|
+
: "";
|
|
766
774
|
report("duplicate-name", candidate.name, `another entity is named ${candidate.name}${builtIn}.`);
|
|
767
775
|
}
|
|
768
776
|
else {
|
|
@@ -869,7 +877,9 @@ function knownVerbs(entities) {
|
|
|
869
877
|
/** Why a role's permission matches nothing in the ontology, or `undefined` when it matches. */
|
|
870
878
|
function unknownPermission(value, { entity, verb }, verbs) {
|
|
871
879
|
if (entity === WILDCARD) {
|
|
872
|
-
|
|
880
|
+
// A wildcard entity does not reach the entities only a named permission covers.
|
|
881
|
+
const matched = verb === WILDCARD ||
|
|
882
|
+
[...verbs].some(([name, own]) => !EXPLICIT_GRANT_ENTITIES.has(name) && own.has(verb));
|
|
873
883
|
return matched ? undefined : `${value} matches nothing: no entity has ${verb}.`;
|
|
874
884
|
}
|
|
875
885
|
const own = verbs.get(entity);
|
|
@@ -961,7 +971,8 @@ function checkPermissions(entities, roles, report) {
|
|
|
961
971
|
name: candidate.name,
|
|
962
972
|
permissions: Array.isArray(candidate.permissions) ? candidate.permissions : [],
|
|
963
973
|
}));
|
|
964
|
-
|
|
974
|
+
// Built-in actions are granted by the workspace's roles when it wants them.
|
|
975
|
+
for (const candidate of entities.filter((entity) => !entity.builtIn)) {
|
|
965
976
|
for (const { name, permission } of candidate.actions) {
|
|
966
977
|
const parsed = parsePermission(permission);
|
|
967
978
|
// An invalid name or permission is already an error; it needs no warning on top.
|
package/dist/authoring.d.ts
CHANGED
|
@@ -4,9 +4,12 @@ import { type AnyEntity } from "./entity.js";
|
|
|
4
4
|
import type { Principal } from "./runtime.js";
|
|
5
5
|
type Report = (code: DiagnosticCode, path: string, message: string) => void;
|
|
6
6
|
export declare function principalFromWire(principal: Principal): Principal;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical definitions feed REST, storage and future transports; the public config stays camelCase.
|
|
9
|
+
* The built-in entities, which every ontology includes, are normalized with the declared ones.
|
|
10
|
+
*/
|
|
11
|
+
export declare function authoringToWire(config: OntologyConfig, builtIns: ReadonlyArray<AnyEntity>, report: Report): {
|
|
9
12
|
readonly config: OntologyConfig;
|
|
10
|
-
readonly
|
|
13
|
+
readonly builtIns: ReadonlyArray<AnyEntity>;
|
|
11
14
|
};
|
|
12
15
|
export {};
|
package/dist/authoring.js
CHANGED
|
@@ -217,8 +217,11 @@ class AuthoringEncoder {
|
|
|
217
217
|
return normalized;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
/**
|
|
221
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Canonical definitions feed REST, storage and future transports; the public config stays camelCase.
|
|
222
|
+
* The built-in entities, which every ontology includes, are normalized with the declared ones.
|
|
223
|
+
*/
|
|
224
|
+
export function authoringToWire(config, builtIns, report) {
|
|
222
225
|
const encoder = new AuthoringEncoder(report);
|
|
223
226
|
const normalized = {
|
|
224
227
|
...config,
|
|
@@ -233,5 +236,5 @@ export function authoringToWire(config, user, report) {
|
|
|
233
236
|
permissions: config.member.permissions.map(permissionToWire),
|
|
234
237
|
};
|
|
235
238
|
}
|
|
236
|
-
return { config: normalized,
|
|
239
|
+
return { config: normalized, builtIns: builtIns.map((entity) => encoder.entity(entity)) };
|
|
237
240
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type { SignInCheck, SignInMethod, SignInOptions, SignInPolicy } from "./s
|
|
|
34
34
|
export { OntologyReads, OntologyRuntime, OntologyWrites } from "./runtime.js";
|
|
35
35
|
export type { FieldCondition, OntologyRuntimeService, FindOptions, GetOptions, Principal, RecordWhere, StoredRecord, StoredValues, UpsertResult, } from "./runtime.js";
|
|
36
36
|
export { User } from "./user.js";
|
|
37
|
+
export { AgentChannel, AgentChat } from "./agent-chat.js";
|
|
38
|
+
export type { AgentMessage } from "./agent-chat.js";
|
|
37
39
|
export { DEFAULT_QUERY_LIMIT, entityQueryResultSchema, entityQuerySchema, entityQuerySchemas, entityRecordSchema, FILTER_OPERATORS, MANY_QUANTIFIERS, MAX_IN_VALUES, MAX_QUERY_LIMIT, OrderBy, Query, queryFields, queryJsonSchema, queryLinks, QueryResult, queryResultJsonSchema, queryResultSchema, SortDirection, WHERE_COMBINATORS, } from "./query.js";
|
|
38
40
|
export type { EntityQuerySchemas, FieldFilter, FilterOperator, JsonRecord, ManyFilter, QueryField, QueryLink, StorageType, Where, } from "./query.js";
|
|
39
41
|
export { InvalidQuery, InvalidValues, MemberNotFound, PrincipalRejected, StoredRecordMismatch, UnknownAction, UnknownRole, ValidationIssue, WriteOutsideAction, } from "./runtime/errors.js";
|
|
@@ -52,10 +54,10 @@ export type { DeriveApiOptions, OntologyApi } from "./service/api.js";
|
|
|
52
54
|
export { CurrentPrincipal } from "./service/authentication.js";
|
|
53
55
|
export { deployedConnections, LocalConnectionContextError, localConnections, OIDC_TOKEN_HEADER, parseLocalConnectionContext, readLocalConnectionContext, RUNTIME_CONTEXT_FD, } from "./service/connections.js";
|
|
54
56
|
export type { ConnectionAudit, ConnectionTransportFactory, LocalConnection, LocalConnectionContext, } from "./service/connections.js";
|
|
55
|
-
export { configFromEnv, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, RUNTIME_IDENTITY_ENVIRONMENT_VARIABLES, } from "./service/config.js";
|
|
57
|
+
export { configFromEnv, AGENT_HOST_VARIABLE, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, RUNTIME_IDENTITY_ENVIRONMENT_VARIABLES, } from "./service/config.js";
|
|
56
58
|
export type { ActionCommitted, AfterCommitHook, NeonAuthEnvironment, OntologyEnvironment, OntologyServiceConfig, } from "./service/config.js";
|
|
57
59
|
export { NotInvited, OntologyBackend } from "./service/runtime-contract.js";
|
|
58
|
-
export type { AgentIdentity, AgentMember, AgentPairing, AgentStore, GeneratedActionsProvider, IdentityProvider, OAuthClient, OAuthClientAuthMethod, OAuthGrant, OAuthRequest, OAuthStore, OAuthToken, OAuthTokenKind, OntologyServiceRuntime, RunActionInput, SignInIdentity, UsedOAuthToken, } from "./service/runtime-contract.js";
|
|
60
|
+
export type { AgentIdentity, AgentMember, AgentPairing, AgentStore, SeenChat, GeneratedActionsProvider, IdentityProvider, OAuthClient, OAuthClientAuthMethod, OAuthGrant, OAuthRequest, OAuthStore, OAuthToken, OAuthTokenKind, OntologyServiceRuntime, RunActionInput, SignInIdentity, UsedOAuthToken, } from "./service/runtime-contract.js";
|
|
59
61
|
export { createOntologyService, ONTOLOGY_HASH_HEADER, REQUEST_ID_HEADER, } from "./service/service.js";
|
|
60
62
|
export { AGENT_PATH } from "./service/agent/server.js";
|
|
61
63
|
export { ACT_AS_HEADER } from "./service/runtime-identity.js";
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { member, role } from "./role.js";
|
|
|
17
17
|
export { DEFAULT_SIGN_IN_POLICY, SIGN_IN_METHODS, SIGN_IN_POLICIES } from "./sign-in.js";
|
|
18
18
|
export { OntologyReads, OntologyRuntime, OntologyWrites } from "./runtime.js";
|
|
19
19
|
export { User } from "./user.js";
|
|
20
|
+
export { AgentChannel, AgentChat } from "./agent-chat.js";
|
|
20
21
|
// The query language of `find` and of the service's list, query and traversal routes.
|
|
21
22
|
export { DEFAULT_QUERY_LIMIT, entityQueryResultSchema, entityQuerySchema, entityQuerySchemas, entityRecordSchema, FILTER_OPERATORS, MANY_QUANTIFIERS, MAX_IN_VALUES, MAX_QUERY_LIMIT, OrderBy, Query, queryFields, queryJsonSchema, queryLinks, QueryResult, queryResultJsonSchema, queryResultSchema, SortDirection, WHERE_COMBINATORS, } from "./query.js";
|
|
22
23
|
// The SQL runtime: data access, the action pipeline, generated actions, history and principals.
|
|
@@ -31,7 +32,7 @@ export { makeOntologyRuntime } from "./runtime/sql-runtime.js";
|
|
|
31
32
|
export { BASE_PATH, deriveApi } from "./service/api.js";
|
|
32
33
|
export { CurrentPrincipal } from "./service/authentication.js";
|
|
33
34
|
export { deployedConnections, LocalConnectionContextError, localConnections, OIDC_TOKEN_HEADER, parseLocalConnectionContext, readLocalConnectionContext, RUNTIME_CONTEXT_FD, } from "./service/connections.js";
|
|
34
|
-
export { configFromEnv, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, RUNTIME_IDENTITY_ENVIRONMENT_VARIABLES, } from "./service/config.js";
|
|
35
|
+
export { configFromEnv, AGENT_HOST_VARIABLE, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, RUNTIME_IDENTITY_ENVIRONMENT_VARIABLES, } from "./service/config.js";
|
|
35
36
|
export { NotInvited, OntologyBackend } from "./service/runtime-contract.js";
|
|
36
37
|
export { createOntologyService, ONTOLOGY_HASH_HEADER, REQUEST_ID_HEADER, } from "./service/service.js";
|
|
37
38
|
// The app's own agent: pairing channel accounts with members, and acting as them.
|
package/dist/permissions.d.ts
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
* Permissions are `<entity>:<verb>` strings. The verbs are `read`, the generated `create`, `update`,
|
|
3
3
|
* `delete`, `link` and `unlink`, and the snake_case name of an authored action. A role may use `*` for
|
|
4
4
|
* the entity (`*:read`), the verb (`lead:*`) or both (`*:*`); what an action or a read requires never
|
|
5
|
-
* holds a wildcard.
|
|
5
|
+
* holds a wildcard. A wildcard entity does not cover the entities in {@link EXPLICIT_GRANT_ENTITIES}.
|
|
6
6
|
*/
|
|
7
7
|
/** Any entity or any verb, in a role's permissions. */
|
|
8
8
|
export declare const WILDCARD = "*";
|
|
9
9
|
/** The verb of every read: lists, queries, records, history, traversals and `include`. */
|
|
10
10
|
export declare const READ_VERB = "read";
|
|
11
|
+
/**
|
|
12
|
+
* Entities only a permission that names them covers: `*:read` or `*:*` grant nothing on them. The
|
|
13
|
+
* built-in agent chat is in every ontology, and member's default `*:read` would otherwise show anyone
|
|
14
|
+
* who signs in the workspace's chats and who is in them; `agent_chat:read` or `agent_chat:*` does.
|
|
15
|
+
*/
|
|
16
|
+
export declare const EXPLICIT_GRANT_ENTITIES: ReadonlySet<string>;
|
|
11
17
|
/** What the built-in `member` role holds when `defineOntology` does not say: reading everything. */
|
|
12
18
|
export declare const DEFAULT_MEMBER_PERMISSIONS: ReadonlyArray<string>;
|
|
13
19
|
export interface ParsedPermission {
|
|
@@ -20,7 +26,10 @@ export interface ParsedPermission {
|
|
|
20
26
|
export declare function parsePermission(value: unknown): ParsedPermission | undefined;
|
|
21
27
|
/** `lead:changeStatus`. */
|
|
22
28
|
export declare function permissionOf(entity: string, verb: string): string;
|
|
23
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Whether a granted permission, which may hold wildcards, covers the required one. A wildcard entity
|
|
31
|
+
* covers every entity but those in {@link EXPLICIT_GRANT_ENTITIES}.
|
|
32
|
+
*/
|
|
24
33
|
export declare function grants(granted: string, required: string): boolean;
|
|
25
34
|
/** Anything that holds permissions: a principal, or the reader of a catalog. */
|
|
26
35
|
export interface PermissionHolder {
|