@pikku/core 0.12.63 → 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 +368 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +42 -27
- package/dist/permissions.js +101 -203
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/testing/service-tests.js +37 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
- package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
- package/dist/wirings/ai-agent/index.d.ts +2 -1
- package/dist/wirings/ai-agent/index.js +2 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +5 -41
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +3 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- 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-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +4 -12
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- 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/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -3
- package/package.json +6 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +160 -373
- package/src/permissions.ts +137 -279
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/testing/service-tests.ts +49 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
- package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
- package/src/wirings/ai-agent/index.ts +5 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +9 -40
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +4 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- 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-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +7 -14
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- 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/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- package/src/wirings/oauth2/oauth2-routes.ts +0 -234
|
@@ -1,14 +1,123 @@
|
|
|
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
|
+
import { ForbiddenError } from '../../errors/errors.js';
|
|
5
|
+
import { verifyScopes } from '../../scopes.js';
|
|
4
6
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
5
7
|
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
|
|
6
8
|
import { randomUUID } from './ai-agent-utils.js';
|
|
7
9
|
import { streamAIAgent } from './ai-agent-stream.js';
|
|
8
10
|
import { runAIAgent } from './ai-agent-runner.js';
|
|
9
11
|
import { resolveNamespace, ContextAwareRPCService, } from '../../wirings/rpc/rpc-runner.js';
|
|
12
|
+
import { getOrCreatePackageSingletonServices } from '../../wirings/rpc/addon-runner.js';
|
|
10
13
|
import { resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
|
|
11
14
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
15
|
+
/**
|
|
16
|
+
* The ownership key for a thread/run: the trusted principal (the authenticated
|
|
17
|
+
* `session.userId` for `'user'` scope, or `session.orgId` for `'org'` scope)
|
|
18
|
+
* composed with the client-supplied `resourceId` as a sub-partition —
|
|
19
|
+
* `principal:resourceId`. Because the principal is always the prefix, a client
|
|
20
|
+
* `resourceId` can sub-divide within the caller's own boundary but can never
|
|
21
|
+
* widen access to another user's or org's threads.
|
|
22
|
+
*
|
|
23
|
+
* Idempotent: a value that is already within the caller's namespace (top-level
|
|
24
|
+
* re-entry, sub-agent recursion, or resume, which all pass an already-composite
|
|
25
|
+
* `resourceId`) is returned unchanged rather than double-composed.
|
|
26
|
+
*
|
|
27
|
+
* `'org'` scope with no session org throws {@link ForbiddenError} — falling back
|
|
28
|
+
* to a bare/shared key would leak across organizations. Sessionless `'user'`
|
|
29
|
+
* wirings have no trusted principal and fall back to the requested `resourceId`
|
|
30
|
+
* as a best-effort partition key.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveOwnerResourceId(params, sessionScope, requestedResourceId) {
|
|
33
|
+
const session = params.sessionService?.get();
|
|
34
|
+
let principal;
|
|
35
|
+
if ((sessionScope ?? 'user') === 'org') {
|
|
36
|
+
if (!session?.orgId) {
|
|
37
|
+
throw new ForbiddenError('This agent is org-scoped but the session has no organization');
|
|
38
|
+
}
|
|
39
|
+
principal = session.orgId;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
principal = session?.userId;
|
|
43
|
+
}
|
|
44
|
+
if (!principal)
|
|
45
|
+
return requestedResourceId;
|
|
46
|
+
if (requestedResourceId === principal ||
|
|
47
|
+
requestedResourceId.startsWith(`${principal}:`)) {
|
|
48
|
+
return requestedResourceId;
|
|
49
|
+
}
|
|
50
|
+
return `${principal}:${requestedResourceId}`;
|
|
51
|
+
}
|
|
52
|
+
/** The declared {@link SessionScope} for an agent, defaulting to `'user'`. */
|
|
53
|
+
export function agentSessionScope(agentName) {
|
|
54
|
+
return resolveAgent(agentName).agent.sessionScope ?? 'user';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Enforce that a stored thread/run belongs to the caller's owner resource,
|
|
58
|
+
* throwing {@link ForbiddenError} (without echoing the id, to avoid an
|
|
59
|
+
* existence oracle) on a mismatch.
|
|
60
|
+
*/
|
|
61
|
+
export function assertResourceOwner(ownerResourceId, storedResourceId, kind) {
|
|
62
|
+
if (storedResourceId !== ownerResourceId) {
|
|
63
|
+
throw new ForbiddenError(`Not authorized to access this ${kind}`);
|
|
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
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Non-forgeable brand for the sub-agent approval marker. Only framework code
|
|
115
|
+
* (the delegating sub-agent tools below) sets this Symbol on a tool result; a
|
|
116
|
+
* sub-agent's LLM-shaped `result.object` — plain JSON — cannot carry a Symbol,
|
|
117
|
+
* so `checkForApprovals` trusts the brand rather than the `__approvalRequired`
|
|
118
|
+
* string key to decide an approval must be surfaced.
|
|
119
|
+
*/
|
|
120
|
+
export const APPROVAL_REQUIRED = Symbol('pikku.ai.approvalRequired');
|
|
12
121
|
export class ToolApprovalRequired extends PikkuError {
|
|
13
122
|
toolCallId;
|
|
14
123
|
toolName;
|
|
@@ -102,6 +211,47 @@ export const resolveAgent = (agentName) => {
|
|
|
102
211
|
}
|
|
103
212
|
throw new Error(`AI agent not found: ${agentName}`);
|
|
104
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
|
+
}
|
|
105
255
|
export async function buildInstructions(agentName, packageName) {
|
|
106
256
|
const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName];
|
|
107
257
|
const parts = [];
|
|
@@ -112,7 +262,7 @@ export async function buildInstructions(agentName, packageName) {
|
|
|
112
262
|
if (meta?.goal)
|
|
113
263
|
parts.push(meta.goal);
|
|
114
264
|
let instructions = parts.join('\n\n');
|
|
115
|
-
if (meta?.tools?.length) {
|
|
265
|
+
if (meta?.tools?.length || meta?.workflows?.length) {
|
|
116
266
|
instructions +=
|
|
117
267
|
'\n\nTool usage rules:\n' +
|
|
118
268
|
'- Act immediately with the information given. Do NOT ask clarifying questions unless a required field is truly missing.\n' +
|
|
@@ -151,6 +301,7 @@ export function createScopedChannel(parent, agentName, session) {
|
|
|
151
301
|
toolCallId: event.toolCallId,
|
|
152
302
|
toolName: event.toolName,
|
|
153
303
|
args: event.args,
|
|
304
|
+
...(event.reason !== undefined ? { reason: event.reason } : {}),
|
|
154
305
|
runId: event.runId,
|
|
155
306
|
});
|
|
156
307
|
return;
|
|
@@ -217,11 +368,17 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
217
368
|
missingRpcs.push(toolName);
|
|
218
369
|
continue;
|
|
219
370
|
}
|
|
220
|
-
// 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).
|
|
221
375
|
if (fnMeta.permissions?.length) {
|
|
222
376
|
if (!session)
|
|
223
377
|
continue;
|
|
224
|
-
const
|
|
378
|
+
const funcConfig = pikkuFuncId
|
|
379
|
+
? pikkuState(resolvedPkg, 'function', 'functions').get(pikkuFuncId)
|
|
380
|
+
: undefined;
|
|
381
|
+
const allowed = await checkAuthPermissions(funcConfig?.permissions, session, singletonServices, resolvedPkg);
|
|
225
382
|
if (!allowed)
|
|
226
383
|
continue;
|
|
227
384
|
}
|
|
@@ -244,13 +401,22 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
244
401
|
if (funcConfig?.approvalDescription) {
|
|
245
402
|
const descFn = funcConfig.approvalDescription;
|
|
246
403
|
const capturedPkg = resolvedPkg;
|
|
404
|
+
const capturedAddonConfig = resolved?.addonConfig;
|
|
405
|
+
const capturedNamespace = toolName.includes(':')
|
|
406
|
+
? toolName.slice(0, toolName.indexOf(':'))
|
|
407
|
+
: null;
|
|
247
408
|
approvalDescriptionFn = async (input) => {
|
|
248
409
|
let services = singletonServices;
|
|
249
410
|
if (capturedPkg) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
411
|
+
const addonInstance = capturedNamespace
|
|
412
|
+
? {
|
|
413
|
+
namespace: capturedNamespace,
|
|
414
|
+
secretOverrides: capturedAddonConfig?.secretOverrides,
|
|
415
|
+
variableOverrides: capturedAddonConfig?.variableOverrides,
|
|
416
|
+
credentialOverrides: capturedAddonConfig?.credentialOverrides,
|
|
417
|
+
}
|
|
418
|
+
: undefined;
|
|
419
|
+
services = await getOrCreatePackageSingletonServices(capturedPkg, singletonServices, addonInstance);
|
|
254
420
|
}
|
|
255
421
|
return descFn(services, input);
|
|
256
422
|
};
|
|
@@ -280,17 +446,20 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
280
446
|
singletonServices.logger.warn(`Sub-agent '${subAgentName}' not found in agent registry`);
|
|
281
447
|
continue;
|
|
282
448
|
}
|
|
283
|
-
// 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.
|
|
284
451
|
if (subMeta.permissions?.length) {
|
|
285
452
|
if (!session)
|
|
286
453
|
continue;
|
|
287
|
-
const
|
|
454
|
+
const subAgent = pikkuState(null, 'agent', 'agents').get(subAgentName);
|
|
455
|
+
const allowed = await checkAuthPermissions(subAgent?.permissions, session, singletonServices);
|
|
288
456
|
if (!allowed)
|
|
289
457
|
continue;
|
|
290
458
|
}
|
|
291
459
|
tools.push({
|
|
292
460
|
name: subAgentName,
|
|
293
461
|
description: subMeta.description,
|
|
462
|
+
forwardsApproval: true,
|
|
294
463
|
inputSchema: {
|
|
295
464
|
type: 'object',
|
|
296
465
|
properties: {
|
|
@@ -339,6 +508,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
339
508
|
const resultText = await streamAIAgent(subAgentName, { message, threadId, resourceId }, effectiveChannel, params, agentSessionMap, streamContext.options);
|
|
340
509
|
if (subChannel.approvals.length > 0) {
|
|
341
510
|
return {
|
|
511
|
+
[APPROVAL_REQUIRED]: true,
|
|
342
512
|
__approvalRequired: true,
|
|
343
513
|
toolName: subAgentName,
|
|
344
514
|
args: toolInput,
|
|
@@ -359,6 +529,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
359
529
|
if (result.status === 'suspended' &&
|
|
360
530
|
result.pendingApprovals?.length) {
|
|
361
531
|
return {
|
|
532
|
+
[APPROVAL_REQUIRED]: true,
|
|
362
533
|
__approvalRequired: true,
|
|
363
534
|
toolName: subAgentName,
|
|
364
535
|
args: toolInput,
|
|
@@ -367,6 +538,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
367
538
|
toolCallId: a.toolCallId,
|
|
368
539
|
toolName: a.toolName,
|
|
369
540
|
args: a.args,
|
|
541
|
+
reason: a.reason,
|
|
370
542
|
runId: a.runId,
|
|
371
543
|
})),
|
|
372
544
|
};
|
|
@@ -376,6 +548,64 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
376
548
|
});
|
|
377
549
|
}
|
|
378
550
|
}
|
|
551
|
+
const metaWorkflows = meta.workflows;
|
|
552
|
+
if (metaWorkflows?.length) {
|
|
553
|
+
const workflowMetaMap = pikkuState(null, 'workflows', 'meta');
|
|
554
|
+
const functionMeta = pikkuState(null, 'function', 'meta');
|
|
555
|
+
const schemas = pikkuState(null, 'misc', 'schemas');
|
|
556
|
+
for (const workflowName of metaWorkflows) {
|
|
557
|
+
const wfMeta = workflowMetaMap[workflowName];
|
|
558
|
+
if (!wfMeta) {
|
|
559
|
+
missingRpcs.push(workflowName);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const inputSchemaName = wfMeta.pikkuFuncId
|
|
563
|
+
? functionMeta[wfMeta.pikkuFuncId]?.inputSchemaName
|
|
564
|
+
: undefined;
|
|
565
|
+
let inputSchema = inputSchemaName
|
|
566
|
+
? schemas.get(inputSchemaName)
|
|
567
|
+
: undefined;
|
|
568
|
+
if (!inputSchema ||
|
|
569
|
+
(typeof inputSchema === 'object' &&
|
|
570
|
+
inputSchema.type === 'object' &&
|
|
571
|
+
!inputSchema.properties)) {
|
|
572
|
+
inputSchema = { type: 'object', properties: {} };
|
|
573
|
+
}
|
|
574
|
+
tools.push({
|
|
575
|
+
name: workflowName,
|
|
576
|
+
description: wfMeta.description || workflowName,
|
|
577
|
+
inputSchema,
|
|
578
|
+
execute: async (toolInput) => {
|
|
579
|
+
const workflowService = singletonServices.workflowService;
|
|
580
|
+
if (!workflowService) {
|
|
581
|
+
throw new Error(`workflowService is not configured — cannot run workflow tool '${workflowName}'`);
|
|
582
|
+
}
|
|
583
|
+
const wire = params.sessionService
|
|
584
|
+
? { ...createMiddlewareSessionWireProps(params.sessionService) }
|
|
585
|
+
: {};
|
|
586
|
+
const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
|
|
587
|
+
return workflowService.runToCompletion(workflowName, toolInput, rpcService, { wire: { type: 'internal' } });
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// A tool's execute() can throw (bad input, RPC failure, DB error, ...). The AI
|
|
593
|
+
// SDK catches that at the tool-call boundary and turns it into a conversational
|
|
594
|
+
// "tool error" reply — without this, the exception never reaches pikku's own
|
|
595
|
+
// logger, so a failing tool is undiagnosable server-side. Log unconditionally,
|
|
596
|
+
// even when no aiMiddleware afterToolCall hook is registered to see it.
|
|
597
|
+
for (const tool of tools) {
|
|
598
|
+
const originalExecute = tool.execute;
|
|
599
|
+
tool.execute = async (toolInput) => {
|
|
600
|
+
try {
|
|
601
|
+
return await originalExecute(toolInput);
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
singletonServices.logger.error(`AI agent tool '${tool.name}' threw during execute()`, err);
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
379
609
|
const hasToolHooks = aiMiddlewares?.some((mw) => mw.beforeToolCall || mw.afterToolCall);
|
|
380
610
|
if (hasToolHooks) {
|
|
381
611
|
for (const tool of tools) {
|
|
@@ -434,6 +664,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
434
664
|
export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
|
|
435
665
|
const singletonServices = getSingletonServices();
|
|
436
666
|
const { agent, packageName, resolvedName } = resolveAgent(agentName);
|
|
667
|
+
await assertAgentAuthorized(agent, params, packageName);
|
|
437
668
|
let agentRunner = singletonServices.aiAgentRunner;
|
|
438
669
|
if (!agentRunner) {
|
|
439
670
|
throw new AIProviderNotConfiguredError();
|
|
@@ -458,10 +689,17 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
|
|
|
458
689
|
? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
|
|
459
690
|
: undefined;
|
|
460
691
|
if (storage) {
|
|
692
|
+
let thread = null;
|
|
461
693
|
try {
|
|
462
|
-
await storage.getThread(threadId);
|
|
694
|
+
thread = await storage.getThread(threadId);
|
|
463
695
|
}
|
|
464
696
|
catch {
|
|
697
|
+
thread = null;
|
|
698
|
+
}
|
|
699
|
+
if (thread) {
|
|
700
|
+
assertResourceOwner(input.resourceId, thread.resourceId, 'thread');
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
465
703
|
await storage.createThread(input.resourceId, { threadId });
|
|
466
704
|
}
|
|
467
705
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
|
|
2
|
-
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, } 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';
|
|
@@ -32,6 +32,10 @@ function extractStructuredAssistantOutput(object) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
34
34
|
const sessionMap = agentSessionMap ?? new Map();
|
|
35
|
+
input = {
|
|
36
|
+
...input,
|
|
37
|
+
resourceId: resolveOwnerResourceId(params, agentSessionScope(agentName), input.resourceId),
|
|
38
|
+
};
|
|
35
39
|
const { agent, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, input, params, sessionMap);
|
|
36
40
|
runnerParams.agentId = agentName;
|
|
37
41
|
const singletonServices = getSingletonServices();
|
|
@@ -281,6 +285,7 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
281
285
|
const run = await aiRunState.getRun(runId);
|
|
282
286
|
if (!run)
|
|
283
287
|
throw new Error(`No run found for runId ${runId}`);
|
|
288
|
+
assertResourceOwner(resolveOwnerResourceId(params, agentSessionScope(run.agentName), run.resourceId), run.resourceId, 'run');
|
|
284
289
|
if (expectedAgentName && run.agentName !== expectedAgentName) {
|
|
285
290
|
throw new Error(`Run ${runId} belongs to agent '${run.agentName}', not '${expectedAgentName}'`);
|
|
286
291
|
}
|
|
@@ -288,6 +293,10 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
288
293
|
throw new Error(`Run ${runId} is not suspended (status: ${run.status})`);
|
|
289
294
|
}
|
|
290
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);
|
|
291
300
|
const { storage } = resolveMemoryServices(agent, singletonServices);
|
|
292
301
|
const memoryConfig = agent.memory;
|
|
293
302
|
const agentRunner = singletonServices.aiAgentRunner;
|
|
@@ -3,9 +3,9 @@ import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
|
3
3
|
import { randomUUID } from './ai-agent-utils.js';
|
|
4
4
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
|
|
5
5
|
import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
|
|
6
|
-
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
|
|
6
|
+
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertAgentAuthorized, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, } from './ai-agent-prepare.js';
|
|
7
7
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
8
|
-
function createPersistingChannel(parent, storage, threadId) {
|
|
8
|
+
function createPersistingChannel(parent, storage, threadId, logger) {
|
|
9
9
|
let fullText = '';
|
|
10
10
|
let stepText = '';
|
|
11
11
|
let stepGenerativeUI = null;
|
|
@@ -51,6 +51,22 @@ function createPersistingChannel(parent, storage, threadId) {
|
|
|
51
51
|
await storage.saveMessages(threadId, messages);
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* `send` is synchronous and so cannot await the flush. A rejection would have
|
|
56
|
+
* nothing to propagate to and would take the process down as an unhandled
|
|
57
|
+
* rejection — a model reusing a toolCallId, which is a primary key in AI
|
|
58
|
+
* storage, is enough to trigger it. Persistence from inside `send` is
|
|
59
|
+
* therefore best-effort: the run carries on, and the awaited `flush()` on the
|
|
60
|
+
* suspend paths still surfaces failures to its caller.
|
|
61
|
+
*/
|
|
62
|
+
const flushDetached = () => {
|
|
63
|
+
void flushStep().catch((error) => {
|
|
64
|
+
logger?.error('Failed to persist agent messages', {
|
|
65
|
+
threadId,
|
|
66
|
+
error,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
54
70
|
const channel = {
|
|
55
71
|
channelId: parent.channelId,
|
|
56
72
|
openingData: parent.openingData,
|
|
@@ -97,10 +113,10 @@ function createPersistingChannel(parent, storage, threadId) {
|
|
|
97
113
|
totalUsage.outputTokens += event.tokens.output;
|
|
98
114
|
if (event.model)
|
|
99
115
|
totalUsage.model = event.model;
|
|
100
|
-
|
|
116
|
+
flushDetached();
|
|
101
117
|
break;
|
|
102
118
|
case 'done':
|
|
103
|
-
|
|
119
|
+
flushDetached();
|
|
104
120
|
break;
|
|
105
121
|
}
|
|
106
122
|
}
|
|
@@ -213,14 +229,23 @@ export function checkForApprovals(stepResult, tools, runId) {
|
|
|
213
229
|
approvals.push(new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args));
|
|
214
230
|
continue;
|
|
215
231
|
}
|
|
232
|
+
// The approval marker is only trusted from a framework tool declared with
|
|
233
|
+
// `forwardsApproval` (the sub-agent delegating tools) AND carrying the
|
|
234
|
+
// non-forgeable `APPROVAL_REQUIRED` Symbol brand. A plain tool's output — or
|
|
235
|
+
// a delegating tool's LLM-shaped `result.object`, which an attacker can
|
|
236
|
+
// influence — is plain JSON and can never carry the Symbol, so it can never
|
|
237
|
+
// forge an approval/suspension.
|
|
238
|
+
if (!toolDef?.forwardsApproval) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
216
241
|
const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
|
|
217
242
|
if (tr?.result &&
|
|
218
243
|
typeof tr.result === 'object' &&
|
|
219
|
-
|
|
244
|
+
APPROVAL_REQUIRED in tr.result) {
|
|
220
245
|
const r = tr.result;
|
|
221
246
|
if (r.subApprovals?.length) {
|
|
222
247
|
for (const sub of r.subApprovals) {
|
|
223
|
-
approvals.push(new ToolApprovalRequired(sub.toolCallId, r.toolName, r.args,
|
|
248
|
+
approvals.push(new ToolApprovalRequired(sub.toolCallId, r.toolName, r.args, sub.reason, sub.toolName, sub.args, r.agentRunId));
|
|
224
249
|
}
|
|
225
250
|
}
|
|
226
251
|
else {
|
|
@@ -341,16 +366,32 @@ function handleCredentialRequests(requests, runId, channel, aiRunState, persisti
|
|
|
341
366
|
suspendReason: 'credential',
|
|
342
367
|
pendingApprovals,
|
|
343
368
|
});
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
369
|
+
// The __credentialRequired tool result is suppressed from the stream, so
|
|
370
|
+
// credential-request events (with the runId needed for /resume) are the
|
|
371
|
+
// client's signal to show Connect/Ignore buttons — mirroring how
|
|
372
|
+
// approval-request suspensions work.
|
|
373
|
+
for (const req of requests) {
|
|
374
|
+
channel.send({
|
|
375
|
+
type: 'credential-request',
|
|
376
|
+
toolCallId: req.toolCallId,
|
|
377
|
+
toolName: req.toolName,
|
|
378
|
+
args: req.args,
|
|
379
|
+
credentialName: req.credentialName,
|
|
380
|
+
credentialType: req.credentialType,
|
|
381
|
+
connectUrl: req.connectUrl,
|
|
382
|
+
runId,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
347
385
|
channel.send({ type: 'done' });
|
|
348
386
|
channel.close();
|
|
349
387
|
})();
|
|
350
388
|
}
|
|
351
389
|
export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
|
|
352
390
|
const sessionMap = agentSessionMap ?? new Map();
|
|
353
|
-
const normalizedInput =
|
|
391
|
+
const normalizedInput = {
|
|
392
|
+
...input,
|
|
393
|
+
resourceId: resolveOwnerResourceId(params, agentSessionScope(agentName), input.resourceId),
|
|
394
|
+
};
|
|
354
395
|
const streamContext = { channel, options };
|
|
355
396
|
// delegateState is attached after prepareAgentRun resolves the agent config
|
|
356
397
|
const { agent, packageName, resolvedName, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, normalizedInput, params, sessionMap, streamContext);
|
|
@@ -408,6 +449,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
408
449
|
createdAt: new Date(),
|
|
409
450
|
updatedAt: new Date(),
|
|
410
451
|
});
|
|
452
|
+
options?.onRunCreated?.(runId);
|
|
411
453
|
if (storage) {
|
|
412
454
|
await storage.saveMessages(threadId, [userMessage]);
|
|
413
455
|
}
|
|
@@ -443,10 +485,26 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
443
485
|
...streamMiddleware,
|
|
444
486
|
],
|
|
445
487
|
});
|
|
446
|
-
const persistingChannel = createPersistingChannel(channel, storage, threadId);
|
|
488
|
+
const persistingChannel = createPersistingChannel(channel, storage, threadId, singletonServices.logger);
|
|
447
489
|
const wrappedChannel = allChannelMiddleware.length > 0
|
|
448
490
|
? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
|
|
449
491
|
: persistingChannel;
|
|
492
|
+
// Tool results carrying the __credentialRequired marker must never reach
|
|
493
|
+
// the client or persisted history: the run suspends with credential-request
|
|
494
|
+
// events instead (mirroring approvals), and leaving the tool call
|
|
495
|
+
// unresulted is what lets the client resume it after connecting.
|
|
496
|
+
const credentialFilteredChannel = {
|
|
497
|
+
...wrappedChannel,
|
|
498
|
+
send: (event) => {
|
|
499
|
+
if (event.type === 'tool-result' &&
|
|
500
|
+
event.result !== null &&
|
|
501
|
+
typeof event.result === 'object' &&
|
|
502
|
+
'__credentialRequired' in event.result) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
wrappedChannel.send(event);
|
|
506
|
+
},
|
|
507
|
+
};
|
|
450
508
|
// In delegate mode (default), suppress parent's text from reaching the client
|
|
451
509
|
// AFTER a sub-agent has been called. If the parent responds directly (no delegation),
|
|
452
510
|
// its text goes through normally. Sub-agent text bypasses this path entirely
|
|
@@ -458,16 +516,16 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
458
516
|
}
|
|
459
517
|
const outputChannel = isDelegateMode
|
|
460
518
|
? {
|
|
461
|
-
...
|
|
519
|
+
...credentialFilteredChannel,
|
|
462
520
|
send: (event) => {
|
|
463
521
|
if (delegateState.delegated &&
|
|
464
522
|
(event.type === 'text-delta' || event.type === 'reasoning-delta'))
|
|
465
523
|
return;
|
|
466
|
-
|
|
524
|
+
credentialFilteredChannel.send(event);
|
|
467
525
|
},
|
|
468
526
|
delegateState,
|
|
469
527
|
}
|
|
470
|
-
:
|
|
528
|
+
: credentialFilteredChannel;
|
|
471
529
|
try {
|
|
472
530
|
const loopResult = await runStreamStepLoop({
|
|
473
531
|
agent,
|
|
@@ -531,12 +589,18 @@ export async function resumeAIAgent(input, channel, params, options) {
|
|
|
531
589
|
if (!run) {
|
|
532
590
|
throw new Error(`No run found for runId ${input.runId}`);
|
|
533
591
|
}
|
|
592
|
+
assertResourceOwner(resolveOwnerResourceId(params, agentSessionScope(run.agentName), run.resourceId), run.resourceId, 'run');
|
|
534
593
|
const pending = run.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId);
|
|
535
594
|
if (!pending) {
|
|
536
595
|
throw new Error(`No pending approval for toolCallId ${input.toolCallId} on run ${input.runId}`);
|
|
537
596
|
}
|
|
538
|
-
await aiRunState.resolveApproval(input.toolCallId, input.approved ? 'approved' : 'denied');
|
|
539
597
|
const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
|
|
598
|
+
// Gate before resolving the approval: recording it is a persisted side
|
|
599
|
+
// effect, so an unauthorized caller must not reach it. Run ownership alone is
|
|
600
|
+
// not enough — a grant revoked while the run was suspended must stop the
|
|
601
|
+
// caller from approving its pending tool calls.
|
|
602
|
+
await assertAgentAuthorized(agent, params, packageName);
|
|
603
|
+
await aiRunState.resolveApproval(input.toolCallId, input.approved ? 'approved' : 'denied');
|
|
540
604
|
const { storage } = resolveMemoryServices(agent, singletonServices);
|
|
541
605
|
const memoryConfig = agent.memory;
|
|
542
606
|
const agentRunner = singletonServices.aiAgentRunner;
|
|
@@ -567,6 +631,14 @@ export async function resumeAIAgent(input, channel, params, options) {
|
|
|
567
631
|
},
|
|
568
632
|
]);
|
|
569
633
|
}
|
|
634
|
+
channel.send({
|
|
635
|
+
type: 'tool-result',
|
|
636
|
+
toolCallId: input.toolCallId,
|
|
637
|
+
toolName: pending.type === 'tool-call' || pending.type === 'credential-request'
|
|
638
|
+
? pending.toolName
|
|
639
|
+
: pending.agentName,
|
|
640
|
+
result: denialResult,
|
|
641
|
+
});
|
|
570
642
|
// Check remaining pending approvals
|
|
571
643
|
const updatedRun = await aiRunState.getRun(run.runId);
|
|
572
644
|
const remaining = updatedRun?.pendingApprovals ?? [];
|
|
@@ -756,7 +828,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
756
828
|
...streamMiddleware,
|
|
757
829
|
],
|
|
758
830
|
});
|
|
759
|
-
const persistingChannel = createPersistingChannel(channel, storage, run.threadId);
|
|
831
|
+
const persistingChannel = createPersistingChannel(channel, storage, run.threadId, singletonServices.logger);
|
|
760
832
|
const wrappedChannel = allChannelMiddleware.length > 0
|
|
761
833
|
? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
|
|
762
834
|
: persistingChannel;
|