@cloudhal/agent-runtime 0.0.0-stage → 0.1.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.
@@ -0,0 +1,197 @@
1
+ import type { Actor } from "@cloudhal/core-contracts";
2
+ import type { AgentMode } from "./modes.js";
3
+ import type { Role } from "./roles.js";
4
+ /**
5
+ * Scopes follow `area:verb`; `*` grants everything (the Owner, local stdio, auth-less servers).
6
+ * RFC-0005 §1 adds own-limited variants (`…:own`) and person-only scopes. A stronger scope implies
7
+ * the weaker ones (`implies`).
8
+ */
9
+ export type Scope = "site:read" | "site:write" | "content:read" | "content:write" | "content:write:own" | "content:draft:own" | "content:propose" | "preview:create" | "site:publish" | "site:publish:own" | "site:rollback" | "asset:upload" | "asset:manage" | "analytics:read" | "comments:read" | "comments:moderate" | "comments:moderate:own" | "comments:reply" | "comments:reply:own" | "commenters:data" | "audit:read" | "site:settings" | "members:invite" | "members:manage" | "agents:manage" | "site:create" | "schedules:read" | "schedules:write" | "schedules:write:own" | "releases:publish" | "releases:publish:own" | "automations:manage" | "ai:invoke" | "*";
10
+ /**
11
+ * Tool scopes: what agent keys may hold and what MCP tools require (I7). `*`, and the person-only
12
+ * scopes, are never grantable to a key.
13
+ */
14
+ export declare const KEY_SCOPES: readonly Scope[];
15
+ /** Scopes only a person holds (through their role): never stored on a key, never satisfy a tool. */
16
+ export declare const PERSON_SCOPES: readonly Scope[];
17
+ /** @deprecated use `KEY_SCOPES` (what a key may carry). */
18
+ export declare const ALL_SCOPES: Scope[];
19
+ /** Every scope except `*`. */
20
+ export declare const SCOPES: readonly Scope[];
21
+ export declare function isScope(value: unknown): value is Scope;
22
+ export declare function isKeyScope(value: unknown): value is Scope;
23
+ /** `a` grants at least what `b` grants. */
24
+ export declare function implies(a: Scope, b: Scope): boolean;
25
+ /** The scopes `scope` implies, strongest first (not including itself). */
26
+ export declare function weakerScopes(scope: Scope): Scope[];
27
+ /** Does a set of scopes grant `scope` (directly, through `*`, or through a stronger scope)? */
28
+ export declare function holds(scopes: readonly string[], scope: Scope): boolean;
29
+ /**
30
+ * The own limit an allowed call carries (RFC-0005 §9): the caller holds only an own variant of the
31
+ * tool's scope, so the handler must check the target. `draft` = never-published entries, status
32
+ * draft/review only.
33
+ */
34
+ export type OwnLimit = {
35
+ level: "own" | "draft";
36
+ target: "entry";
37
+ } | {
38
+ level: "own";
39
+ target: "changeset";
40
+ }
41
+ /** RFC-0007 §10.1: each comment's entry must be the person's own (its author, or their agent). */
42
+ | {
43
+ level: "own";
44
+ target: "comment";
45
+ }
46
+ /**
47
+ * RFC-0008 §10.1: every node a release (or a gate) touches must be the person's own — an entry
48
+ * whose author is the person or one of their agents (`ownOnlyChangeSet`'s rule).
49
+ */
50
+ | {
51
+ level: "own";
52
+ target: "release";
53
+ };
54
+ /**
55
+ * What a call does, decided from its arguments (RFC-0007 §10.2), for tools whose policy depends
56
+ * on them (`moderate_comments` may hide in `edits` mode but never approve):
57
+ *
58
+ * | Class | Meaning | plan | manual | edits | auto | bypass (local) |
59
+ * |---|---|---|---|---|---|---|
60
+ * | `read` | changes nothing | ✓ | ✓ | ✓ | ✓ | ✓ |
61
+ * | `advisory` | changes only notes people read before deciding (labels) | ✗ | ✓ | ✓ | ✓ | ✓ |
62
+ * | `reduce` | reversible, can only make something less visible | ✗ | waits | ✓ | ✓ | ✓ |
63
+ * | `publish` | makes content public, or removes it, at a time | ✗ | waits | waits | low-risk in window ✓, else waits | ✓ |
64
+ * | `person` | makes something public | ✗ | waits | waits | waits | **waits** |
65
+ * | `propose` | files a proposal a person accepts or rejects; changes no content (ADR-0028) | ✓ | ✓ | ✓ | ✓ | ✓ |
66
+ *
67
+ * `person` calls wait with cause `tool` in every mode, `bypass` included, and for people calling
68
+ * over MCP too (they are asked in their own client). `publish` (RFC-0008 §10.3) follows ADR-0015's
69
+ * publish rules instead: in `auto` mode the decision carries `autoApprove: "low-risk-release"` and
70
+ * the server applies the low-risk check and the window rule (§10.7) to the release itself.
71
+ * Tools without `classify`, or whose `classify` returns nothing for a call, behave as before.
72
+ */
73
+ export type CallClass = "read" | "advisory" | "reduce" | "publish" | "person" | "propose";
74
+ export interface ToolAnnotations {
75
+ readOnlyHint?: boolean;
76
+ destructiveHint?: boolean;
77
+ idempotentHint?: boolean;
78
+ openWorldHint?: boolean;
79
+ }
80
+ export interface ToolPolicy {
81
+ /** Tool name as exposed over MCP. */
82
+ name: string;
83
+ /** Scopes the caller must hold (any `*` satisfies). */
84
+ scopes: Scope[];
85
+ annotations: ToolAnnotations;
86
+ /** Tools that always need a human decision, independent of scopes. */
87
+ requiresApproval?: boolean;
88
+ /**
89
+ * The own variants of the tool's scope satisfy it with a limit (RFC-0005 §9): `entry` — the
90
+ * handler checks the target entry; `changeset` — a publish must be own-only.
91
+ */
92
+ own?: "entry" | "changeset" | "comment" | "release";
93
+ /** Per-argument policy (RFC-0007 §10.2); `evaluate` receives the call's arguments. */
94
+ classify?: (args: Record<string, unknown>) => CallClass | undefined;
95
+ /**
96
+ * The scopes a `propose` call needs instead of `scopes` (ADR-0028): proposing an edit to a body
97
+ * needs `content:propose`, not the write it proposes.
98
+ */
99
+ proposeScopes?: Scope[];
100
+ }
101
+ export interface Principal {
102
+ actor: Actor;
103
+ scopes: Scope[];
104
+ /** Skill the agent declared it is running (its frontmatter decides extra approvals). */
105
+ skill?: {
106
+ name: string;
107
+ scopes: string[];
108
+ approval: string[];
109
+ } | undefined;
110
+ /** Agents only: how independently it works (ADR-0015). Absent = `edits`; people have none. */
111
+ mode?: AgentMode | undefined;
112
+ /** True for the local stdio server (the developer's own machine); enables `bypass`. */
113
+ local?: boolean | undefined;
114
+ /** Set while `mode` comes from a time-boxed elevation (ADR-0016): the key's own mode and the end. */
115
+ elevation?: {
116
+ from: AgentMode;
117
+ until: string;
118
+ } | undefined;
119
+ /** The API key an agent authenticated with (lets it ask for an elevation of that key). */
120
+ keyId?: string | undefined;
121
+ /**
122
+ * RFC-0008 §11.2: the key is an automation's key (`purpose: "automation"` in its metadata). It
123
+ * works only inside an open run of that automation, except `begin_run` and `get_mode`.
124
+ */
125
+ automationId?: string | undefined;
126
+ /**
127
+ * The person behind the call (RFC-0005): for people themselves, for agents the person who created
128
+ * the key. Absent when there is no membership source (auth-less servers, local stdio): such
129
+ * principals are not role-bound.
130
+ */
131
+ person?: PersonRef | undefined;
132
+ /** Agents only: the key's own scopes before narrowing, so `forSite` can narrow them per site. */
133
+ keyScopes?: readonly string[] | undefined;
134
+ }
135
+ /** A staff member as the policy layer sees them. */
136
+ export interface PersonRef {
137
+ /** Actor id (`user_…`), the form `author` and audit rows use. */
138
+ id: string;
139
+ /** Better Auth user id. */
140
+ userId: string;
141
+ role: Role;
142
+ name?: string | undefined;
143
+ /**
144
+ * RFC-0034 A2: the sites an Editor, Author or Contributor is limited to, each with its role.
145
+ * Absent: the role applies to every site. `forSite` applies it.
146
+ */
147
+ sites?: Readonly<Record<string, Role>> | undefined;
148
+ }
149
+ /** The mode that actually applies: `bypass` only counts locally, remote keys fall back to `edits`. */
150
+ export declare function effectiveMode(principal: Principal): AgentMode | undefined;
151
+ /**
152
+ * Why a call waits for a person (stored on approvals and audit rows). `role`: the caller's role
153
+ * allows less than the call (an Author's publish that is not own-only, RFC-0005 §6).
154
+ */
155
+ export type ApprovalCause = "tool" | "destructive" | "skill" | "manual-mode" | "role";
156
+ export type Decision = {
157
+ kind: "allow";
158
+ limit?: OwnLimit;
159
+ } | {
160
+ kind: "deny";
161
+ reason: string;
162
+ code?: "missing_scope" | "mode" | "skill";
163
+ } | {
164
+ kind: "approval-required";
165
+ reason: string;
166
+ /** Why a person must decide: the tool always asks, it is destructive, the skill asks, or manual mode. */
167
+ cause: ApprovalCause;
168
+ /**
169
+ * `auto` mode: the caller may skip the person when the change is low risk — a publish now
170
+ * (`low-risk-publish`, ADR-0015) or a release inside the key's window (`low-risk-release`,
171
+ * RFC-0008 §10.7).
172
+ */
173
+ autoApprove?: "low-risk-publish" | "low-risk-release";
174
+ /** The own limit the call runs under once approved. */
175
+ limit?: OwnLimit;
176
+ };
177
+ /**
178
+ * The single place that decides whether a tool call may run (ADR-0004). Humans and agents share
179
+ * the same model: scopes come from the role or API key, approvals from annotations and the
180
+ * declared skill. Skills never grant permission — they can only ask for more approval.
181
+ */
182
+ export declare class PolicyEngine {
183
+ #private;
184
+ constructor(tools: ToolPolicy[]);
185
+ tool(name: string): ToolPolicy | undefined;
186
+ /** Every tool, in registration order (a run envelope lists the ones a run may call). */
187
+ tools(): ToolPolicy[];
188
+ evaluate(principal: Principal, toolName: string, options?: {
189
+ confirmed?: boolean;
190
+ args?: Record<string, unknown> | undefined;
191
+ }): Decision;
192
+ }
193
+ /**
194
+ * Default scope sets per actor kind, for principals without a membership source (auth-less
195
+ * servers, tests, local scripts). With auth, a person's scopes come from their role (RFC-0005 I1).
196
+ */
197
+ export declare function defaultScopes(actor: Actor): Scope[];