@pikku/core 0.12.64 → 0.12.66
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 +221 -0
- package/dist/permissions.d.ts +12 -4
- package/dist/permissions.js +11 -32
- package/dist/testing/service-tests.js +37 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +64 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +103 -5
- package/dist/wirings/ai-agent/ai-agent-runner.js +5 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +28 -7
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +29 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/ai-agent/voice-input.js +3 -3
- package/dist/wirings/cli/cli-runner.js +3 -0
- package/dist/wirings/cli/command-parser.d.ts +2 -0
- package/dist/wirings/cli/command-parser.js +59 -2
- package/dist/wirings/credential/credential.types.d.ts +14 -0
- package/dist/wirings/credential/validate-credential-definitions.js +1 -0
- package/dist/wirings/gateway/gateway-runner.js +100 -50
- package/dist/wirings/gateway/gateway.types.d.ts +8 -5
- package/dist/wirings/http/http.types.d.ts +3 -3
- package/dist/wirings/secret/secret.types.d.ts +14 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/variable/validate-variable-definitions.js +2 -0
- package/dist/wirings/variable/variable.types.d.ts +14 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +36 -6
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +16 -0
- package/dist/wirings/workflow/workflow.types.d.ts +0 -2
- package/package.json +2 -1
- package/src/permissions.test.ts +14 -8
- package/src/permissions.ts +14 -36
- package/src/testing/service-tests.ts +49 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +175 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +132 -5
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +7 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +103 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +38 -6
- package/src/wirings/ai-agent/ai-agent.types.ts +29 -0
- package/src/wirings/ai-agent/index.ts +4 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +8 -10
- package/src/wirings/cli/cli-runner.ts +4 -0
- package/src/wirings/cli/command-parser.test.ts +130 -0
- package/src/wirings/cli/command-parser.ts +80 -2
- package/src/wirings/credential/credential.types.ts +14 -0
- package/src/wirings/credential/validate-credential-definitions.ts +1 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
- package/src/wirings/gateway/gateway-runner.ts +114 -68
- package/src/wirings/gateway/gateway.types.ts +7 -9
- package/src/wirings/http/http.types.ts +6 -4
- package/src/wirings/secret/secret.types.ts +14 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/variable/validate-variable-definitions.ts +2 -0
- package/src/wirings/variable/variable.types.ts +14 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +36 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +36 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow.types.ts +0 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,224 @@
|
|
|
1
|
+
## 0.12.66
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 5f19016: Widen the generated agent HTTP surface, and guard attachment downloads against SSRF.
|
|
6
|
+
|
|
7
|
+
`agentCaller` and `agentStreamCaller` declared only `message`, `threadId` and
|
|
8
|
+
`resourceId` (plus `context` on the stream route), so `attachments`, `model`,
|
|
9
|
+
`temperature` — all accepted by `AIAgentInput` — were unreachable over the
|
|
10
|
+
shipped HTTP contract. No deployed app could send an attachment or a per-request
|
|
11
|
+
model override. Both callers now share an `AgentCallerInput` type covering every
|
|
12
|
+
optional field and forward each one to the RPC.
|
|
13
|
+
|
|
14
|
+
Both callers declare that shape **inline** in the generic position rather than
|
|
15
|
+
behind a shared named alias: the schema extractor only reads type literals there
|
|
16
|
+
and synthesises the schema name from the function name. Behind an alias it
|
|
17
|
+
records an `inputSchemaName` with no schema generated for it, and every agent
|
|
18
|
+
HTTP call then fails at runtime with `MissingSchemaError`.
|
|
19
|
+
|
|
20
|
+
Widening that surface makes caller-supplied attachment URLs reachable, which is
|
|
21
|
+
an SSRF vector: the AI SDK downloads attachment URLs **server-side** whenever the
|
|
22
|
+
model cannot consume them natively, using an unguarded `fetch`. A caller could
|
|
23
|
+
point an attachment at the cloud metadata endpoint or another internal host and
|
|
24
|
+
have the response relayed into the model's context. `VercelAIAgentRunner` now
|
|
25
|
+
passes an `experimental_download` implementation backed by `safeFetch` (which
|
|
26
|
+
refuses private/internal hosts and non-HTTP schemes, and re-validates every
|
|
27
|
+
redirect hop) to both `streamText` and `generateText`. URLs the model supports
|
|
28
|
+
natively are passed through untouched, so the provider still fetches those
|
|
29
|
+
itself.
|
|
30
|
+
|
|
31
|
+
The runner takes an optional `allowedAttachmentHosts` allowlist, carried across
|
|
32
|
+
`withApiKey`. `safeFetch` is now exported from `@pikku/core/safe-fetch`.
|
|
33
|
+
|
|
34
|
+
- 78e4778: Stop a failed message persist during an agent stream from killing the process.
|
|
35
|
+
|
|
36
|
+
The persisting channel flushes from inside `send`, which is synchronous and so cannot await the flush. Any rejection — a dropped storage connection, or a model reusing a `toolCallId`, which is a primary key in AI storage — escaped as an unhandled rejection and took the whole server down. Persistence from `send` is now best-effort and logged; the awaited `flush()` on the suspend paths still surfaces failures to its caller.
|
|
37
|
+
|
|
38
|
+
- 4324652: Scope AI agent thread reads to the calling session.
|
|
39
|
+
|
|
40
|
+
The generated thread-management functions (`getAgentThreads`,
|
|
41
|
+
`getAgentThreadMessages`, `getAgentThreadRuns`, `deleteAgentThread`) keyed purely
|
|
42
|
+
off a caller-supplied `threadId` and treated `resourceId` as an optional filter,
|
|
43
|
+
so omitting it enumerated every tenant's threads.
|
|
44
|
+
- `listThreads` gains an `owners` **authorization constraint** (distinct from the
|
|
45
|
+
`resourceId` filter): an empty array matches nothing, and it is always derived
|
|
46
|
+
from the session, never from input. Implemented across the Kysely, Redis and
|
|
47
|
+
MongoDB agent run services, with LIKE/regex metacharacter escaping so an owner
|
|
48
|
+
id containing `_` or `%` cannot match a foreign owner.
|
|
49
|
+
- The three `threadId`-keyed functions are now guarded by an `isThreadOwner`
|
|
50
|
+
`pikkuPermission` rather than an in-body check. A thread that does not exist is
|
|
51
|
+
denied rather than 404'd, so it is indistinguishable from one owned by someone
|
|
52
|
+
else.
|
|
53
|
+
- New `@pikku/core/ai-agent` helpers: `canAccessThread`, `threadOwnerConstraint`,
|
|
54
|
+
`sessionPrincipals`, `isOwnedByPrincipal`.
|
|
55
|
+
|
|
56
|
+
Services destructured by a wired function are now non-optional inside it.
|
|
57
|
+
|
|
58
|
+
The inspector already aggregated the services used by every wired `func`,
|
|
59
|
+
`permissions` and `middleware` into `RequiredSingletonServices`, but the
|
|
60
|
+
generated function types defaulted their service parameter to the raw `Services`
|
|
61
|
+
— so a service declared `foo?: Foo` still arrived as possibly-undefined, forcing
|
|
62
|
+
`if (!foo) throw new MissingServiceError(...)` guards that could never fire.
|
|
63
|
+
Generated types now expose `WiredSingletonServices` / `WiredServices`
|
|
64
|
+
(`RequiredSingletonServices & Services`) and default the `RequiredServices`
|
|
65
|
+
generic of functions, permissions, middleware, auth and approval-description
|
|
66
|
+
helpers to them. Optionality now means only what it should: "this service may
|
|
67
|
+
not be created, because nothing uses it".
|
|
68
|
+
|
|
69
|
+
- de044f8: Fix the agent tool-list permission filter failing open.
|
|
70
|
+
|
|
71
|
+
`buildToolDefs` filtered permission-gated tools by resolving `checkAuthPermissions` from a function's _metadata_ — a by-name lookup into the `misc/permissions` state that nothing ever populates. It therefore collected no predicate and returned `true`, so every auth-gated tool was offered to the model regardless of session (its input schema and description leaked, and the model could attempt calls that then failed at invocation).
|
|
72
|
+
|
|
73
|
+
`checkAuthPermissions` now takes the live `CorePermissionGroup` from the function/agent config, where the `pikkuAuth` brand actually survives — matching how the agent's own gate and the function runner already resolve permissions by reference. The dead by-name lookup (`getPermissionByName`) is removed. Enforcement on invocation was never affected; this closes the exposure gap in the offered tool list.
|
|
74
|
+
|
|
75
|
+
- cd1a811: warn instead of silently ignoring unknown long CLI options
|
|
76
|
+
|
|
77
|
+
An unknown long option (`--sektion functions` or `--sektion=functions`) was parsed
|
|
78
|
+
into the options object and then silently dropped by the command's input schema —
|
|
79
|
+
the command ran with the real option at its default and produced plausible-but-wrong
|
|
80
|
+
output. Unknown long options are still accepted (forward compatibility is preserved),
|
|
81
|
+
but the parser now records a warning that the runner prints to stderr, e.g.
|
|
82
|
+
`Warning: Unknown option: --sektion (ignored) Did you mean --section?`.
|
|
83
|
+
|
|
84
|
+
- 19fa6f0: Fix `HTTPRouteConfig` and `HTTPRoutesGroupConfig`'s default `PikkuPermission`/`PikkuMiddleware` type parameters under-specifying their own generic arguments (e.g. `CorePikkuPermission<any>` instead of `CorePikkuPermission<any, any, any>`). The missing arguments silently fell back to `CorePikkuPermission`'s own defaults (`CoreServices`, with `schema` optional) instead of `any`, so a project whose generated services type guarantees `schema` is always present (any project using `WiredServices`-style non-optional services) failed to type-check against `defineHTTPRoutes`/`wireHTTPRoutes` with a misleading `index signature` error.
|
|
85
|
+
- b501612: Enforce authorization consistently across `pikku*` primitives.
|
|
86
|
+
- `pikkuAIAgent` now enforces `permissions` (previously accepted but never
|
|
87
|
+
checked) and gains `auth` and `scopes`. Scopes are checked before permissions.
|
|
88
|
+
`auth` defaults to `false`, matching `pikkuSessionlessFunc`, since agents are
|
|
89
|
+
typically invoked from an already-authenticated function or from sessionless
|
|
90
|
+
contexts such as crons and queue workers.
|
|
91
|
+
- `pikkuWorkflowFunc` / `pikkuWorkflowComplexFunc` schema config gains `auth`
|
|
92
|
+
and `scopes` alongside `permissions`.
|
|
93
|
+
- `pikkuScenario` no longer accepts `auth`, `scopes`, or `permissions` —
|
|
94
|
+
scenarios drive the app as actors and authorize per step.
|
|
95
|
+
- `wireGateway` no longer accepts `permissions`. A gateway proxies to an agent,
|
|
96
|
+
so access is governed by normal auth plus the target agent's own rules.
|
|
97
|
+
- Removed the dead `permissions` field from `CoreWorkflow`, which was never read.
|
|
98
|
+
|
|
99
|
+
Closed two paths that reached user code without authorization:
|
|
100
|
+
- Gateway handlers were invoked directly, so a handler's own `auth`, `scopes`
|
|
101
|
+
and `permissions` were never evaluated. Webhook, websocket and listener
|
|
102
|
+
gateways now invoke the handler through the function runner. Handlers are
|
|
103
|
+
sessionless by default (inbound gateway traffic is platform-authenticated by
|
|
104
|
+
the adapter, not session-bearing); declare `auth: true` to require a session.
|
|
105
|
+
A gateway's own `auth` field is now honoured too — it was previously ignored.
|
|
106
|
+
Gateway middleware runs before the gate, so `wire.setSession()` in gateway
|
|
107
|
+
middleware — the idiomatic way to map a verified platform sender to a user —
|
|
108
|
+
is visible to the handler's `auth` and `scopes`.
|
|
109
|
+
- Resuming a suspended agent run (`resumeAIAgentSync`, `resumeAIAgent`) checked
|
|
110
|
+
run ownership but never re-ran the agent's own gate, so a scope or permission
|
|
111
|
+
revoked while a run was suspended did not prevent the caller from resuming it
|
|
112
|
+
and approving its pending tool calls. Both now re-run `assertAgentAuthorized`
|
|
113
|
+
before any state is mutated.
|
|
114
|
+
|
|
115
|
+
- eb37b1e: Fix `voiceInput` middleware losing the runner receiver: it grabbed
|
|
116
|
+
`aiAgentRunner.transcribe` as a bare method reference, so calling it left `this`
|
|
117
|
+
undefined and threw `Cannot read properties of undefined (reading 'getModel')`
|
|
118
|
+
on the first audio attachment. It now calls `aiAgentRunner.transcribe(...)`
|
|
119
|
+
directly, preserving the receiver.
|
|
120
|
+
|
|
121
|
+
## 0.12.65
|
|
122
|
+
|
|
123
|
+
### Patch Changes
|
|
124
|
+
|
|
125
|
+
- 1a86d3f: Fix a fanout collapsing into a single step, and preserve graph node config.
|
|
126
|
+
- A fanout took its `stepName` from the first step of its body. Node ids _are_
|
|
127
|
+
step names, so the loop and that step got the same id and the step overwrote
|
|
128
|
+
the loop: `await Promise.all(users.map(...))` rendered as one plain call, and
|
|
129
|
+
everything after the loop became unreachable. A fanout is not itself a cached
|
|
130
|
+
step, so it no longer borrows a name.
|
|
131
|
+
- A `workflow.sleep` or `workflow.suspend` inside a fanout body was dropped at
|
|
132
|
+
extraction — `FanoutStepMeta.body` was typed RPC-only. It now admits sleep and
|
|
133
|
+
suspend, and the regenerated body emits them.
|
|
134
|
+
- Regenerating a `pikkuWorkflowGraph` dropped `onError`, `retries` and
|
|
135
|
+
`retryDelay` from every node, and graph-level `notes`. All four are honoured
|
|
136
|
+
at runtime, so the round trip silently changed behaviour.
|
|
137
|
+
|
|
138
|
+
- 1a86d3f: Support multi-step fanout bodies in DSL workflows.
|
|
139
|
+
|
|
140
|
+
A `Promise.all(array.map(...))` (or `for...of`) body containing more than one
|
|
141
|
+
`workflow.do` call previously extracted only a single step: `const`-captured
|
|
142
|
+
steps were skipped entirely by the parallel extractor, so a body like
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
await Promise.all(
|
|
146
|
+
users.map(async (u) => {
|
|
147
|
+
const digestData = await workflow.do('Get pipeline', 'getDigestData', {
|
|
148
|
+
userId: u.id,
|
|
149
|
+
})
|
|
150
|
+
await workflow.do('Send digest', 'sendDigestEmail', { ...digestData })
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
produced a graph with `getDigestData` missing and `sendDigestEmail` referencing
|
|
156
|
+
an unregistered variable. `FanoutStepMeta.child` is replaced by
|
|
157
|
+
`FanoutStepMeta.body: RpcStepMeta[]`, holding the per-iteration steps inline in
|
|
158
|
+
the same workflow — no sub-workflow boundary. Per-iteration `const` bindings are
|
|
159
|
+
now registered so later steps in the same iteration can reference them, and the
|
|
160
|
+
sequential path no longer hard-errors on bodies with more than one step.
|
|
161
|
+
|
|
162
|
+
- 1a86d3f: Add `onError` compensation to DSL workflows.
|
|
163
|
+
|
|
164
|
+
A DSL workflow had no way to express error handling at all — `try/catch` is not
|
|
165
|
+
an allowed statement, and step options carried only `retries`/`retryDelay`. A
|
|
166
|
+
step can now name a compensation RPC:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await workflow.do(
|
|
170
|
+
'Charge',
|
|
171
|
+
'chargeCard',
|
|
172
|
+
{ id },
|
|
173
|
+
{
|
|
174
|
+
retries: 3,
|
|
175
|
+
onError: 'refundOrder',
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Semantics mirror a graph node's `onError` exactly: once the step's retries are
|
|
181
|
+
exhausted the handler is invoked with `{ error: { message } }` and the original
|
|
182
|
+
error is still thrown. This is compensation, not recovery — the workflow fails
|
|
183
|
+
either way. The handler runs as its own durable step, so a replay cannot
|
|
184
|
+
compensate twice, and it does not inherit `onError` itself.
|
|
185
|
+
|
|
186
|
+
The handler is materialised as a real graph node, so it is wired like any other
|
|
187
|
+
RPC and the console draws a dashed red "on error" edge to it rather than the
|
|
188
|
+
route being invisible.
|
|
189
|
+
|
|
190
|
+
- 1a86d3f: Stop silently dropping switch cases and spread returns from workflow graphs.
|
|
191
|
+
- A fall-through case (`case 'a': case 'b': ...`) recorded only the last value.
|
|
192
|
+
A run entering on `'a'` therefore appeared to match no case at all. Empty
|
|
193
|
+
clauses now carry through to the entry they fall into — the next non-empty
|
|
194
|
+
case, otherwise `default`, otherwise the switch exit.
|
|
195
|
+
- `return { ...r, extra: 1 }` produced a return node listing only `extra`, so
|
|
196
|
+
the graph claimed an output shape the workflow does not have, with no
|
|
197
|
+
diagnostic. `return r` produced no return node at all. `ReturnStepMeta` now
|
|
198
|
+
records a `spread` list, and the regenerated code emits it.
|
|
199
|
+
|
|
200
|
+
- 1a86d3f: Stop corrupting values when regenerating a workflow from its graph.
|
|
201
|
+
- A numeric `workflow.sleep('Wait', 5000)` came back as `'5000'`, and a numeric
|
|
202
|
+
`retryDelay` likewise. Durations are `string | number`; only strings are
|
|
203
|
+
quoted now.
|
|
204
|
+
- An assignment to a context variable was stored as an opaque `value`, so
|
|
205
|
+
`count = count + 1` regenerated as `count = 'count + 1'` — an expression
|
|
206
|
+
turned into a string literal. `SetStepMeta` now carries a separate
|
|
207
|
+
`expression` field (mirroring `SwitchCaseMeta`), so a string literal and a
|
|
208
|
+
code expression are no longer indistinguishable in the meta.
|
|
209
|
+
- A `next` that was not a single node id was coerced with a string cast: an
|
|
210
|
+
array became the bogus id `'a,b'` and a branch-key record became
|
|
211
|
+
`'[object Object]'`, severing every downstream node. Arrays, key-based
|
|
212
|
+
routing tables and condition lists now each render in their own shape.
|
|
213
|
+
- A `filter`/`some`/`every` node with no `outputVar` emitted
|
|
214
|
+
`const undefined = ...`, which does not parse.
|
|
215
|
+
|
|
216
|
+
- 1a86d3f: Keep a `workflow.sleep` whose duration is only known at runtime (a loop
|
|
217
|
+
variable, a field off the input). The closure evaluates it, so it is legal DSL;
|
|
218
|
+
its source text is recorded as an `expression` and emitted raw when regenerating
|
|
219
|
+
code, as a set step already does.
|
|
220
|
+
- 3d76f51: Add an optional `docsUrl` to `wireSecret`, `wireVariable`, and `wireCredential`, so a console or deploy UI reporting a missing value can link the user to where they obtain it instead of showing a bare identifier.
|
|
221
|
+
|
|
1
222
|
## 0.12.64
|
|
2
223
|
|
|
3
224
|
### Patch Changes
|
package/dist/permissions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CoreServices, CoreUserSession,
|
|
1
|
+
import type { CoreServices, CoreUserSession, PikkuWire } from './types/core.types.js';
|
|
2
2
|
import type { CorePermissionGroup, CorePikkuPermission } from './function/functions.types.js';
|
|
3
3
|
export declare const clearPermissionsCache: () => void;
|
|
4
4
|
/**
|
|
@@ -36,12 +36,14 @@ export declare const addTagPermission: (_tag: string, _permissions: CorePermissi
|
|
|
36
36
|
* A passing global requirement never contributes to the function gate, so a
|
|
37
37
|
* broad global like `signedIn` can't satisfy an admin-only function.
|
|
38
38
|
*/
|
|
39
|
-
export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, }: {
|
|
39
|
+
export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, label, }: {
|
|
40
40
|
funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
|
|
41
41
|
services: CoreServices;
|
|
42
42
|
wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>;
|
|
43
43
|
data: any;
|
|
44
44
|
packageName?: string | null;
|
|
45
|
+
/** What the non-global gate is called in debug logs, e.g. 'function', 'agent'. */
|
|
46
|
+
label?: string;
|
|
45
47
|
}) => Promise<void>;
|
|
46
48
|
/**
|
|
47
49
|
* Checks whether a session passes the auth checks (pikkuAuth only) for a
|
|
@@ -49,10 +51,16 @@ export declare const runPermissions: ({ funcPermissions, services, wire, data, p
|
|
|
49
51
|
* request data which isn't available at filter time. Global auth requirements
|
|
50
52
|
* are included so a filtered list honours app-wide auth.
|
|
51
53
|
*
|
|
52
|
-
*
|
|
54
|
+
* `funcPermissions` is the live {@link CorePermissionGroup} from the function or
|
|
55
|
+
* agent config, not the metadata form: the `pikkuAuth` brand only survives on
|
|
56
|
+
* the actual predicate objects, and the by-name registry those metadata entries
|
|
57
|
+
* would resolve against is never populated. Passing metadata here would silently
|
|
58
|
+
* collect nothing and let every gated tool through.
|
|
59
|
+
*
|
|
60
|
+
* @param funcPermissions - The live permission group from the func/agent config
|
|
53
61
|
* @param session - The user's session
|
|
54
62
|
* @param services - Singleton services
|
|
55
63
|
* @param packageName - Optional package namespace
|
|
56
64
|
* @returns true if the session passes the auth checks (or no auth checks exist)
|
|
57
65
|
*/
|
|
58
|
-
export declare const checkAuthPermissions: (funcPermissions:
|
|
66
|
+
export declare const checkAuthPermissions: (funcPermissions: CorePermissionGroup | undefined, session: CoreUserSession, services: CoreServices, packageName?: string | null) => Promise<boolean>;
|
package/dist/permissions.js
CHANGED
|
@@ -40,26 +40,6 @@ const verifyPermissions = async (permissions, services, data, wire) => {
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
};
|
|
43
|
-
/**
|
|
44
|
-
* Retrieves a registered permission function by its name.
|
|
45
|
-
*
|
|
46
|
-
* This function looks up permissions that was registered with registerPermission.
|
|
47
|
-
* It's used internally by the framework to resolve permission references in metadata.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} name - The unique name (pikkuFuncId) of the permission function.
|
|
50
|
-
* @param {string | null} packageName - Optional package namespace.
|
|
51
|
-
* @returns {CorePikkuPermission | undefined} The permission function, or undefined if not found.
|
|
52
|
-
*
|
|
53
|
-
* @internal
|
|
54
|
-
*/
|
|
55
|
-
const getPermissionByName = (name, packageName = null) => {
|
|
56
|
-
const permissionStore = pikkuState(packageName, 'misc', 'permissions');
|
|
57
|
-
const permission = permissionStore[name];
|
|
58
|
-
if (Array.isArray(permission) && permission.length === 1) {
|
|
59
|
-
return permission[0];
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
};
|
|
63
43
|
const globalPermissionsCache = {};
|
|
64
44
|
export const clearPermissionsCache = () => {
|
|
65
45
|
for (const key of Object.keys(globalPermissionsCache)) {
|
|
@@ -131,7 +111,7 @@ const asGroup = (entry) => typeof entry === 'function' ? { permission: entry } :
|
|
|
131
111
|
* A passing global requirement never contributes to the function gate, so a
|
|
132
112
|
* broad global like `signedIn` can't satisfy an admin-only function.
|
|
133
113
|
*/
|
|
134
|
-
export const runPermissions = async ({ funcPermissions, services, wire, data, packageName = null, }) => {
|
|
114
|
+
export const runPermissions = async ({ funcPermissions, services, wire, data, packageName = null, label = 'function', }) => {
|
|
135
115
|
const globals = resolveGlobalPermissions(packageName);
|
|
136
116
|
for (const entry of globals) {
|
|
137
117
|
if (!(await verifyPermissions(asGroup(entry), services, data, wire))) {
|
|
@@ -145,7 +125,7 @@ export const runPermissions = async ({ funcPermissions, services, wire, data, pa
|
|
|
145
125
|
: funcPermissions;
|
|
146
126
|
if (group && Object.keys(group).length > 0) {
|
|
147
127
|
if (!(await verifyPermissions(group, services, data, wire))) {
|
|
148
|
-
services.logger.debug(
|
|
128
|
+
services.logger.debug(`Permission denied - ${label} permission`);
|
|
149
129
|
throw new ForbiddenError('Permission denied');
|
|
150
130
|
}
|
|
151
131
|
}
|
|
@@ -157,7 +137,13 @@ export const runPermissions = async ({ funcPermissions, services, wire, data, pa
|
|
|
157
137
|
* request data which isn't available at filter time. Global auth requirements
|
|
158
138
|
* are included so a filtered list honours app-wide auth.
|
|
159
139
|
*
|
|
160
|
-
*
|
|
140
|
+
* `funcPermissions` is the live {@link CorePermissionGroup} from the function or
|
|
141
|
+
* agent config, not the metadata form: the `pikkuAuth` brand only survives on
|
|
142
|
+
* the actual predicate objects, and the by-name registry those metadata entries
|
|
143
|
+
* would resolve against is never populated. Passing metadata here would silently
|
|
144
|
+
* collect nothing and let every gated tool through.
|
|
145
|
+
*
|
|
146
|
+
* @param funcPermissions - The live permission group from the func/agent config
|
|
161
147
|
* @param session - The user's session
|
|
162
148
|
* @param services - Singleton services
|
|
163
149
|
* @param packageName - Optional package namespace
|
|
@@ -189,15 +175,8 @@ export const checkAuthPermissions = async (funcPermissions, session, services, p
|
|
|
189
175
|
for (const entry of resolveGlobalPermissions(packageName)) {
|
|
190
176
|
collect(entry);
|
|
191
177
|
}
|
|
192
|
-
if (funcPermissions
|
|
193
|
-
|
|
194
|
-
if (meta.type === 'wire') {
|
|
195
|
-
const permission = getPermissionByName(meta.name, packageName);
|
|
196
|
-
if (permission) {
|
|
197
|
-
collect(permission);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
178
|
+
if (funcPermissions) {
|
|
179
|
+
collect(funcPermissions);
|
|
201
180
|
}
|
|
202
181
|
// No auth permissions = allowed (only data-dependent permissions exist)
|
|
203
182
|
if (authPerms.length === 0)
|
|
@@ -742,6 +742,43 @@ export function defineServiceTests(config) {
|
|
|
742
742
|
const threads = await agentService.listThreads();
|
|
743
743
|
assert.ok(Array.isArray(threads));
|
|
744
744
|
});
|
|
745
|
+
if (services.aiStorageService) {
|
|
746
|
+
const storageFactory = services.aiStorageService;
|
|
747
|
+
// The `owners` constraint is what keeps the generated thread-management
|
|
748
|
+
// functions from leaking across tenants: a caller may only list threads
|
|
749
|
+
// owned by one of their session principals, matching the
|
|
750
|
+
// `principal:sub-partition` composition resolveOwnerResourceId writes.
|
|
751
|
+
test('listThreads scopes to the given owners, including sub-partitions', async () => {
|
|
752
|
+
const storage = await storageFactory();
|
|
753
|
+
await storage.createThread('owner-alice');
|
|
754
|
+
await storage.createThread('owner-alice:project-1');
|
|
755
|
+
await storage.createThread('owner-bob:secret');
|
|
756
|
+
const threads = await agentService.listThreads({
|
|
757
|
+
owners: ['owner-alice'],
|
|
758
|
+
});
|
|
759
|
+
const ids = threads.map((t) => t.resourceId);
|
|
760
|
+
assert.ok(ids.includes('owner-alice'));
|
|
761
|
+
assert.ok(ids.includes('owner-alice:project-1'));
|
|
762
|
+
assert.ok(!ids.some((id) => id.startsWith('owner-bob')), "another owner's threads must not be listed");
|
|
763
|
+
});
|
|
764
|
+
test('listThreads with an owner does not match a lookalike prefix', async () => {
|
|
765
|
+
const storage = await storageFactory();
|
|
766
|
+
await storage.createThread('owner-al');
|
|
767
|
+
await storage.createThread('owner-alice-evil:p');
|
|
768
|
+
const threads = await agentService.listThreads({
|
|
769
|
+
owners: ['owner-al'],
|
|
770
|
+
});
|
|
771
|
+
const ids = threads.map((t) => t.resourceId);
|
|
772
|
+
assert.ok(ids.includes('owner-al'));
|
|
773
|
+
assert.ok(!ids.includes('owner-alice-evil:p'));
|
|
774
|
+
});
|
|
775
|
+
test('listThreads with an empty owners list returns nothing', async () => {
|
|
776
|
+
const storage = await storageFactory();
|
|
777
|
+
await storage.createThread('owner-empty-check');
|
|
778
|
+
const threads = await agentService.listThreads({ owners: [] });
|
|
779
|
+
assert.deepEqual(threads, []);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
745
782
|
test('getThread returns null for missing', async () => {
|
|
746
783
|
const thread = await agentService.getThread('missing-thread');
|
|
747
784
|
assert.equal(thread, null);
|
|
@@ -34,6 +34,49 @@ export declare function agentSessionScope(agentName: string): SessionScope;
|
|
|
34
34
|
* existence oracle) on a mismatch.
|
|
35
35
|
*/
|
|
36
36
|
export declare function assertResourceOwner(ownerResourceId: string, storedResourceId: string, kind: 'thread' | 'run'): void;
|
|
37
|
+
/** A session's trusted principals, in the order they may be read as. */
|
|
38
|
+
export declare function sessionPrincipals(session: {
|
|
39
|
+
userId?: string;
|
|
40
|
+
orgId?: string;
|
|
41
|
+
} | undefined): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Whether `storedResourceId` belongs to `principal` under the composition
|
|
44
|
+
* {@link resolveOwnerResourceId} performs — either the bare principal or one of
|
|
45
|
+
* its `principal:` sub-partitions. The `:` is required so that `alice` does not
|
|
46
|
+
* match a `alice-evil:…` lookalike.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isOwnedByPrincipal(storedResourceId: string, principal: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The `owners` constraint to pass to `AgentRunService.listThreads` for a caller.
|
|
51
|
+
*
|
|
52
|
+
* Returns `undefined` — not `[]` — for a session with no principal, because an
|
|
53
|
+
* empty list means "match nothing" whereas a sessionless deployment (agent
|
|
54
|
+
* `no-auth`) has no ownership model to constrain by. Keeping that carve-out here
|
|
55
|
+
* rather than at each call site stops it from being re-derived inconsistently.
|
|
56
|
+
*/
|
|
57
|
+
export declare function threadOwnerConstraint(session: {
|
|
58
|
+
userId?: string;
|
|
59
|
+
orgId?: string;
|
|
60
|
+
} | undefined): string[] | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Whether a stored thread/run may be read by the caller. Shaped as a predicate
|
|
63
|
+
* so it can back a `pikkuPermission` — authorization belongs in a function's
|
|
64
|
+
* `permissions` field, never in its body.
|
|
65
|
+
*
|
|
66
|
+
* Unlike {@link assertResourceOwner}, which compares against a single composed
|
|
67
|
+
* owner key on the run path, this guards the thread-management reads where the
|
|
68
|
+
* caller supplies only a `threadId` — so ownership has to be derived from the
|
|
69
|
+
* session rather than from the request.
|
|
70
|
+
*
|
|
71
|
+
* A session with no principal means the deployment opted out of authorization
|
|
72
|
+
* (agent `no-auth`), so there is no ownership model to enforce and access is not
|
|
73
|
+
* gated — mirroring {@link resolveOwnerResourceId}'s sessionless fallback to a
|
|
74
|
+
* bare resourceId.
|
|
75
|
+
*/
|
|
76
|
+
export declare function canAccessThread(storedResourceId: string, session: {
|
|
77
|
+
userId?: string;
|
|
78
|
+
orgId?: string;
|
|
79
|
+
} | undefined): boolean;
|
|
37
80
|
export type StreamAIAgentOptions = {
|
|
38
81
|
requiresToolApproval?: 'all' | 'explicit' | false;
|
|
39
82
|
/**
|
|
@@ -93,6 +136,27 @@ export declare const resolveAgent: (agentName: string) => {
|
|
|
93
136
|
packageName: string | null;
|
|
94
137
|
resolvedName: string;
|
|
95
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Enforces an agent's own authorization before it runs: session presence
|
|
141
|
+
* (`auth`), then `scopes`, then `permissions`.
|
|
142
|
+
*
|
|
143
|
+
* The ordering mirrors the function runner — scopes are an AND gate checked
|
|
144
|
+
* first, so they can only ever narrow access, and a missing scope short-circuits
|
|
145
|
+
* before any permission function does I/O.
|
|
146
|
+
*
|
|
147
|
+
* `auth` follows `pikkuSessionlessFunc` rather than `pikkuFunc`: a session is
|
|
148
|
+
* required only when `auth: true` is set explicitly. An agent is normally
|
|
149
|
+
* reached from a function that has already enforced its own auth, and agents are
|
|
150
|
+
* also run from genuinely sessionless contexts (crons, queue workers), so
|
|
151
|
+
* requiring a session by default would reject those without adding a meaningful
|
|
152
|
+
* gate. `scopes` and `permissions` are always enforced when declared.
|
|
153
|
+
*
|
|
154
|
+
* Globals are evaluated here too (via {@link runPermissions}) rather than being
|
|
155
|
+
* assumed to have already run: an agent is reachable from entry points that do
|
|
156
|
+
* not go through the function runner, and re-evaluating an AND gate of
|
|
157
|
+
* side-effect-free predicates is idempotent.
|
|
158
|
+
*/
|
|
159
|
+
export declare function assertAgentAuthorized(agent: CoreAIAgent, params: RunAIAgentParams, packageName: string | null): Promise<void>;
|
|
96
160
|
export declare function buildInstructions(agentName: string, packageName: string | null): Promise<string>;
|
|
97
161
|
export type ScopedChannel = AIStreamChannel & {
|
|
98
162
|
approvals: Array<{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
2
|
-
import { checkAuthPermissions } from '../../permissions.js';
|
|
2
|
+
import { checkAuthPermissions, runPermissions } from '../../permissions.js';
|
|
3
3
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
4
4
|
import { ForbiddenError } from '../../errors/errors.js';
|
|
5
|
+
import { verifyScopes } from '../../scopes.js';
|
|
5
6
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
6
7
|
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
|
|
7
8
|
import { randomUUID } from './ai-agent-utils.js';
|
|
@@ -62,6 +63,53 @@ export function assertResourceOwner(ownerResourceId, storedResourceId, kind) {
|
|
|
62
63
|
throw new ForbiddenError(`Not authorized to access this ${kind}`);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
/** A session's trusted principals, in the order they may be read as. */
|
|
67
|
+
export function sessionPrincipals(session) {
|
|
68
|
+
return [session?.userId, session?.orgId].filter((principal) => Boolean(principal));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether `storedResourceId` belongs to `principal` under the composition
|
|
72
|
+
* {@link resolveOwnerResourceId} performs — either the bare principal or one of
|
|
73
|
+
* its `principal:` sub-partitions. The `:` is required so that `alice` does not
|
|
74
|
+
* match a `alice-evil:…` lookalike.
|
|
75
|
+
*/
|
|
76
|
+
export function isOwnedByPrincipal(storedResourceId, principal) {
|
|
77
|
+
return (storedResourceId === principal ||
|
|
78
|
+
storedResourceId.startsWith(`${principal}:`));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The `owners` constraint to pass to `AgentRunService.listThreads` for a caller.
|
|
82
|
+
*
|
|
83
|
+
* Returns `undefined` — not `[]` — for a session with no principal, because an
|
|
84
|
+
* empty list means "match nothing" whereas a sessionless deployment (agent
|
|
85
|
+
* `no-auth`) has no ownership model to constrain by. Keeping that carve-out here
|
|
86
|
+
* rather than at each call site stops it from being re-derived inconsistently.
|
|
87
|
+
*/
|
|
88
|
+
export function threadOwnerConstraint(session) {
|
|
89
|
+
const principals = sessionPrincipals(session);
|
|
90
|
+
return principals.length > 0 ? principals : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether a stored thread/run may be read by the caller. Shaped as a predicate
|
|
94
|
+
* so it can back a `pikkuPermission` — authorization belongs in a function's
|
|
95
|
+
* `permissions` field, never in its body.
|
|
96
|
+
*
|
|
97
|
+
* Unlike {@link assertResourceOwner}, which compares against a single composed
|
|
98
|
+
* owner key on the run path, this guards the thread-management reads where the
|
|
99
|
+
* caller supplies only a `threadId` — so ownership has to be derived from the
|
|
100
|
+
* session rather than from the request.
|
|
101
|
+
*
|
|
102
|
+
* A session with no principal means the deployment opted out of authorization
|
|
103
|
+
* (agent `no-auth`), so there is no ownership model to enforce and access is not
|
|
104
|
+
* gated — mirroring {@link resolveOwnerResourceId}'s sessionless fallback to a
|
|
105
|
+
* bare resourceId.
|
|
106
|
+
*/
|
|
107
|
+
export function canAccessThread(storedResourceId, session) {
|
|
108
|
+
const principals = sessionPrincipals(session);
|
|
109
|
+
if (principals.length === 0)
|
|
110
|
+
return true;
|
|
111
|
+
return principals.some((principal) => isOwnedByPrincipal(storedResourceId, principal));
|
|
112
|
+
}
|
|
65
113
|
/**
|
|
66
114
|
* Non-forgeable brand for the sub-agent approval marker. Only framework code
|
|
67
115
|
* (the delegating sub-agent tools below) sets this Symbol on a tool result; a
|
|
@@ -163,6 +211,47 @@ export const resolveAgent = (agentName) => {
|
|
|
163
211
|
}
|
|
164
212
|
throw new Error(`AI agent not found: ${agentName}`);
|
|
165
213
|
};
|
|
214
|
+
/**
|
|
215
|
+
* Enforces an agent's own authorization before it runs: session presence
|
|
216
|
+
* (`auth`), then `scopes`, then `permissions`.
|
|
217
|
+
*
|
|
218
|
+
* The ordering mirrors the function runner — scopes are an AND gate checked
|
|
219
|
+
* first, so they can only ever narrow access, and a missing scope short-circuits
|
|
220
|
+
* before any permission function does I/O.
|
|
221
|
+
*
|
|
222
|
+
* `auth` follows `pikkuSessionlessFunc` rather than `pikkuFunc`: a session is
|
|
223
|
+
* required only when `auth: true` is set explicitly. An agent is normally
|
|
224
|
+
* reached from a function that has already enforced its own auth, and agents are
|
|
225
|
+
* also run from genuinely sessionless contexts (crons, queue workers), so
|
|
226
|
+
* requiring a session by default would reject those without adding a meaningful
|
|
227
|
+
* gate. `scopes` and `permissions` are always enforced when declared.
|
|
228
|
+
*
|
|
229
|
+
* Globals are evaluated here too (via {@link runPermissions}) rather than being
|
|
230
|
+
* assumed to have already run: an agent is reachable from entry points that do
|
|
231
|
+
* not go through the function runner, and re-evaluating an AND gate of
|
|
232
|
+
* side-effect-free predicates is idempotent.
|
|
233
|
+
*/
|
|
234
|
+
export async function assertAgentAuthorized(agent, params, packageName) {
|
|
235
|
+
const session = params.sessionService
|
|
236
|
+
? await params.sessionService.get()
|
|
237
|
+
: undefined;
|
|
238
|
+
if (agent.auth === true && !session) {
|
|
239
|
+
throw new ForbiddenError('Authentication required');
|
|
240
|
+
}
|
|
241
|
+
verifyScopes(agent.scopes, session);
|
|
242
|
+
const singletonServices = getSingletonServices();
|
|
243
|
+
const wire = params.sessionService
|
|
244
|
+
? createMiddlewareSessionWireProps(params.sessionService)
|
|
245
|
+
: { session: undefined };
|
|
246
|
+
await runPermissions({
|
|
247
|
+
funcPermissions: agent.permissions,
|
|
248
|
+
services: singletonServices,
|
|
249
|
+
wire: wire,
|
|
250
|
+
data: {},
|
|
251
|
+
packageName,
|
|
252
|
+
label: 'agent',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
166
255
|
export async function buildInstructions(agentName, packageName) {
|
|
167
256
|
const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName];
|
|
168
257
|
const parts = [];
|
|
@@ -279,11 +368,17 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
279
368
|
missingRpcs.push(toolName);
|
|
280
369
|
continue;
|
|
281
370
|
}
|
|
282
|
-
// Filter out tools the user doesn't have auth for
|
|
371
|
+
// Filter out tools the user doesn't have auth for. The `pikkuAuth` brand
|
|
372
|
+
// only survives on the live permission objects in the function config, so
|
|
373
|
+
// the check reads those rather than the metadata (whose by-name registry
|
|
374
|
+
// is never populated, which would let every gated tool through).
|
|
283
375
|
if (fnMeta.permissions?.length) {
|
|
284
376
|
if (!session)
|
|
285
377
|
continue;
|
|
286
|
-
const
|
|
378
|
+
const funcConfig = pikkuFuncId
|
|
379
|
+
? pikkuState(resolvedPkg, 'function', 'functions').get(pikkuFuncId)
|
|
380
|
+
: undefined;
|
|
381
|
+
const allowed = await checkAuthPermissions(funcConfig?.permissions, session, singletonServices, resolvedPkg);
|
|
287
382
|
if (!allowed)
|
|
288
383
|
continue;
|
|
289
384
|
}
|
|
@@ -351,11 +446,13 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
351
446
|
singletonServices.logger.warn(`Sub-agent '${subAgentName}' not found in agent registry`);
|
|
352
447
|
continue;
|
|
353
448
|
}
|
|
354
|
-
// Filter out sub-agents the user doesn't have auth for
|
|
449
|
+
// Filter out sub-agents the user doesn't have auth for, reading the live
|
|
450
|
+
// agent config for the same reason the tool path does.
|
|
355
451
|
if (subMeta.permissions?.length) {
|
|
356
452
|
if (!session)
|
|
357
453
|
continue;
|
|
358
|
-
const
|
|
454
|
+
const subAgent = pikkuState(null, 'agent', 'agents').get(subAgentName);
|
|
455
|
+
const allowed = await checkAuthPermissions(subAgent?.permissions, session, singletonServices);
|
|
359
456
|
if (!allowed)
|
|
360
457
|
continue;
|
|
361
458
|
}
|
|
@@ -567,6 +664,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
567
664
|
export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
|
|
568
665
|
const singletonServices = getSingletonServices();
|
|
569
666
|
const { agent, packageName, resolvedName } = resolveAgent(agentName);
|
|
667
|
+
await assertAgentAuthorized(agent, params, packageName);
|
|
570
668
|
let agentRunner = singletonServices.aiAgentRunner;
|
|
571
669
|
if (!agentRunner) {
|
|
572
670
|
throw new AIProviderNotConfiguredError();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
|
|
2
|
-
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, } from './ai-agent-prepare.js';
|
|
2
|
+
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertAgentAuthorized, } from './ai-agent-prepare.js';
|
|
3
3
|
import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
|
|
4
4
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
5
5
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
@@ -293,6 +293,10 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
293
293
|
throw new Error(`Run ${runId} is not suspended (status: ${run.status})`);
|
|
294
294
|
}
|
|
295
295
|
const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
|
|
296
|
+
// Resuming re-runs the agent, so it re-runs the agent's gate. Run ownership
|
|
297
|
+
// alone is not enough: a grant revoked while the run was suspended must stop
|
|
298
|
+
// the caller from approving its pending tool calls.
|
|
299
|
+
await assertAgentAuthorized(agent, params, packageName);
|
|
296
300
|
const { storage } = resolveMemoryServices(agent, singletonServices);
|
|
297
301
|
const memoryConfig = agent.memory;
|
|
298
302
|
const agentRunner = singletonServices.aiAgentRunner;
|