@pikku/core 0.12.137 → 0.12.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/dev/hot-reload.js +5 -2
- package/dist/env.d.ts +18 -0
- package/dist/env.js +60 -1
- package/dist/function/abort-scope.d.ts +6 -5
- package/dist/function/abort-scope.js +6 -17
- package/dist/function/function-runner.js +2 -1
- package/dist/middleware/addon-scope-cap.d.ts +19 -0
- package/dist/middleware/addon-scope-cap.js +34 -0
- package/dist/middleware/auth-apikey.js +1 -1
- package/dist/middleware/auth-bearer.js +1 -1
- package/dist/middleware/auth-cookie.js +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/schema.js +3 -1
- package/dist/scopes.d.ts +8 -0
- package/dist/scopes.js +23 -0
- package/dist/services/content-service.d.ts +13 -1
- package/dist/services/in-memory-queue-service.js +4 -2
- package/dist/services/in-memory-workflow-service.js +7 -6
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +1 -1
- package/dist/services/local-content-request-handler.js +21 -60
- package/dist/services/local-content.d.ts +45 -1
- package/dist/services/local-content.js +202 -18
- package/dist/services/local-variables.d.ts +5 -0
- package/dist/services/local-variables.js +7 -2
- package/dist/services/queue-webhook-service.js +1 -1
- package/dist/services/scoped-content-service.d.ts +35 -0
- package/dist/services/scoped-content-service.js +171 -0
- package/dist/services/webhook-service.d.ts +2 -2
- package/dist/services/webhook-service.js +6 -4
- package/dist/types/core.types.d.ts +10 -1
- package/dist/types/state.types.d.ts +8 -0
- package/dist/utils/hmac.d.ts +25 -10
- package/dist/utils/hmac.js +202 -24
- package/dist/wirings/addon/addon-reach.d.ts +3 -0
- package/dist/wirings/addon/addon-reach.js +21 -0
- package/dist/wirings/addon/addon-runner.d.ts +6 -0
- package/dist/wirings/addon/addon-runner.js +13 -0
- package/dist/wirings/addon/define-screens.d.ts +32 -0
- package/dist/wirings/addon/define-screens.js +5 -0
- package/dist/wirings/addon/index.d.ts +2 -0
- package/dist/wirings/addon/index.js +1 -0
- package/dist/wirings/addon/wire-addon.d.ts +27 -0
- package/dist/wirings/addon/wire-addon.js +4 -0
- package/dist/wirings/agent/agent-agui.js +65 -62
- package/dist/wirings/agent/agent-interrupt.d.ts +4 -1
- package/dist/wirings/agent/agent-interrupt.js +3 -3
- package/dist/wirings/agent/agent-memory.js +5 -6
- package/dist/wirings/agent/agent-model-config.js +2 -1
- package/dist/wirings/agent/agent-prepare.js +24 -13
- package/dist/wirings/agent/agent-stream.js +38 -34
- package/dist/wirings/agent/agent.types.d.ts +8 -1
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -1
- package/dist/wirings/channel/local/local-channel-runner.js +6 -4
- package/dist/wirings/channel/local/local-eventhub-service.d.ts +1 -0
- package/dist/wirings/channel/local/local-eventhub-service.js +4 -1
- package/dist/wirings/cli/channel/cli-channel-runner.js +7 -2
- package/dist/wirings/cli/channel/cli-raw-client-runner.js +8 -4
- package/dist/wirings/cli/cli-runner.js +1 -1
- package/dist/wirings/gateway/gateway-runner.js +3 -3
- package/dist/wirings/http/http-runner.js +6 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +4 -0
- package/dist/wirings/rpc/rpc-runner.js +41 -2
- package/dist/wirings/rpc/rpc-types.d.ts +6 -0
- package/dist/wirings/scheduler/scheduler-runner.js +1 -1
- package/dist/wirings/scope/define-scope.d.ts +19 -0
- package/dist/wirings/scope/define-scope.js +19 -0
- package/dist/wirings/scope/index.d.ts +2 -2
- package/dist/wirings/scope/index.js +2 -2
- package/dist/wirings/trigger/trigger-runner.js +3 -1
- package/dist/wirings/trigger/webhook-source-runner.js +5 -2
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +3 -2
- package/dist/wirings/workflow/workflow-invocation-id.js +85 -8
- package/package.json +3 -1
- package/src/public-surface.json +65 -18
|
@@ -86,7 +86,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
|
|
|
86
86
|
flush: flushStep,
|
|
87
87
|
close: () => parent.close(),
|
|
88
88
|
sendBinary: (data) => parent.sendBinary(data),
|
|
89
|
-
send: (event) => {
|
|
89
|
+
send: async (event) => {
|
|
90
90
|
// Accumulated whether or not storage is configured: `fullText` is what
|
|
91
91
|
// the client was streamed, and an interrupted run has to be able to
|
|
92
92
|
// report the fragment it got through even with persistence turned off.
|
|
@@ -149,7 +149,7 @@ function createPersistingChannel(parent, storage, threadId, logger) {
|
|
|
149
149
|
break;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
parent.send(event);
|
|
152
|
+
await parent.send(event);
|
|
153
153
|
},
|
|
154
154
|
setState: (s) => parent.setState(s),
|
|
155
155
|
getState: () => parent.getState(),
|
|
@@ -267,7 +267,7 @@ async function runStreamStepLoop(params) {
|
|
|
267
267
|
if (stopped)
|
|
268
268
|
break;
|
|
269
269
|
}
|
|
270
|
-
channel.send({ type: 'step-start', stepNumber: step });
|
|
270
|
+
await channel.send({ type: 'step-start', stepNumber: step });
|
|
271
271
|
const stepResult = await agentRunner.stream(runnerParams, streamChannel);
|
|
272
272
|
for (const mw of agentMiddlewares) {
|
|
273
273
|
if (mw.afterStep) {
|
|
@@ -422,10 +422,10 @@ function handleApprovals(approvals, runId, channel, agentRunState, persistingCha
|
|
|
422
422
|
reason: err.reason,
|
|
423
423
|
runId,
|
|
424
424
|
};
|
|
425
|
-
channel.send(approvalEvent);
|
|
425
|
+
await channel.send(approvalEvent);
|
|
426
426
|
}
|
|
427
|
-
channel.send({ type: 'done' });
|
|
428
|
-
channel.close();
|
|
427
|
+
await channel.send({ type: 'done' });
|
|
428
|
+
await channel.close();
|
|
429
429
|
})();
|
|
430
430
|
}
|
|
431
431
|
function handleCredentialRequests(requests, runId, channel, agentRunState, persistingChannel) {
|
|
@@ -446,7 +446,7 @@ function handleCredentialRequests(requests, runId, channel, agentRunState, persi
|
|
|
446
446
|
pendingApprovals,
|
|
447
447
|
});
|
|
448
448
|
for (const req of requests) {
|
|
449
|
-
channel.send({
|
|
449
|
+
await channel.send({
|
|
450
450
|
type: 'credential-request',
|
|
451
451
|
toolCallId: req.toolCallId,
|
|
452
452
|
toolName: req.toolName,
|
|
@@ -457,8 +457,8 @@ function handleCredentialRequests(requests, runId, channel, agentRunState, persi
|
|
|
457
457
|
runId,
|
|
458
458
|
});
|
|
459
459
|
}
|
|
460
|
-
channel.send({ type: 'done' });
|
|
461
|
-
channel.close();
|
|
460
|
+
await channel.send({ type: 'done' });
|
|
461
|
+
await channel.close();
|
|
462
462
|
})();
|
|
463
463
|
}
|
|
464
464
|
export async function streamAgent(agentName, input, channel, params, agentSessionMap, options) {
|
|
@@ -487,8 +487,12 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
487
487
|
createdAt: new Date(),
|
|
488
488
|
updatedAt: new Date(),
|
|
489
489
|
});
|
|
490
|
-
channel.send({
|
|
491
|
-
|
|
490
|
+
await channel.send({
|
|
491
|
+
type: 'suspended',
|
|
492
|
+
reason: 'rpc-missing',
|
|
493
|
+
missingRpcs,
|
|
494
|
+
});
|
|
495
|
+
await channel.send({ type: 'done' });
|
|
492
496
|
return '';
|
|
493
497
|
}
|
|
494
498
|
const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
|
|
@@ -512,7 +516,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
512
516
|
// knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
|
|
513
517
|
const transcript = sharedNotes[SPOKEN_TRANSCRIPT];
|
|
514
518
|
if (typeof transcript === 'string') {
|
|
515
|
-
channel.send({ type: 'transcript', text: transcript });
|
|
519
|
+
await channel.send({ type: 'transcript', text: transcript });
|
|
516
520
|
}
|
|
517
521
|
// knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
|
|
518
522
|
const lastModified = modifiedMessages[modifiedMessages.length - 1];
|
|
@@ -615,7 +619,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
615
619
|
});
|
|
616
620
|
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
617
621
|
await outputChannel.send({ type: 'done' });
|
|
618
|
-
channel.close();
|
|
622
|
+
await channel.close();
|
|
619
623
|
return persistingChannel.fullText;
|
|
620
624
|
}
|
|
621
625
|
catch (err) {
|
|
@@ -631,14 +635,14 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
631
635
|
if (storage) {
|
|
632
636
|
trackInterruptNote(threadId, persistOrphanedToolResults(interruptHandle, storage, threadId));
|
|
633
637
|
}
|
|
634
|
-
channel.send({
|
|
638
|
+
await channel.send({
|
|
635
639
|
type: 'interrupted',
|
|
636
640
|
runId,
|
|
637
641
|
text: persistingChannel.fullText,
|
|
638
642
|
reason: interruption?.reason ?? 'user',
|
|
639
643
|
});
|
|
640
|
-
channel.send({ type: 'done' });
|
|
641
|
-
channel.close();
|
|
644
|
+
await channel.send({ type: 'done' });
|
|
645
|
+
await channel.close();
|
|
642
646
|
return persistingChannel.fullText;
|
|
643
647
|
}
|
|
644
648
|
for (const mw of agentMiddlewares) {
|
|
@@ -657,12 +661,12 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
657
661
|
status: 'failed',
|
|
658
662
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
659
663
|
});
|
|
660
|
-
channel.send({
|
|
664
|
+
await channel.send({
|
|
661
665
|
type: 'error',
|
|
662
666
|
message: err instanceof Error ? err.message : String(err),
|
|
663
667
|
});
|
|
664
|
-
channel.send({ type: 'done' });
|
|
665
|
-
channel.close();
|
|
668
|
+
await channel.send({ type: 'done' });
|
|
669
|
+
await channel.close();
|
|
666
670
|
return persistingChannel.fullText;
|
|
667
671
|
}
|
|
668
672
|
finally {
|
|
@@ -763,7 +767,7 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
763
767
|
},
|
|
764
768
|
]);
|
|
765
769
|
}
|
|
766
|
-
channel.send({
|
|
770
|
+
await channel.send({
|
|
767
771
|
type: 'tool-result',
|
|
768
772
|
toolCallId: input.toolCallId,
|
|
769
773
|
toolName: pending.type === 'tool-call' || pending.type === 'credential-request'
|
|
@@ -774,8 +778,8 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
774
778
|
const updatedRun = await agentRunState.getRun(run.runId);
|
|
775
779
|
const remaining = updatedRun?.pendingApprovals ?? [];
|
|
776
780
|
if (remaining.length > 0) {
|
|
777
|
-
channel.send({ type: 'done' });
|
|
778
|
-
channel.close();
|
|
781
|
+
await channel.send({ type: 'done' });
|
|
782
|
+
await channel.close();
|
|
779
783
|
return;
|
|
780
784
|
}
|
|
781
785
|
await agentRunState.updateRun(run.runId, { status: 'running' });
|
|
@@ -793,7 +797,7 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
793
797
|
throw new Error(`No pending approval on sub-agent run ${pending.agentRunId}`);
|
|
794
798
|
}
|
|
795
799
|
const subChannel = createScopedChannel(channel, subRun.agentName, 'resume');
|
|
796
|
-
channel.send({
|
|
800
|
+
await channel.send({
|
|
797
801
|
type: 'agent-call',
|
|
798
802
|
agentName: subRun.agentName,
|
|
799
803
|
session: 'resume',
|
|
@@ -804,7 +808,7 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
804
808
|
toolCallId: subPending.toolCallId,
|
|
805
809
|
approved: true,
|
|
806
810
|
}, subChannel, params, options);
|
|
807
|
-
channel.send({
|
|
811
|
+
await channel.send({
|
|
808
812
|
type: 'agent-result',
|
|
809
813
|
agentName: subRun.agentName,
|
|
810
814
|
session: 'resume',
|
|
@@ -871,7 +875,7 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
871
875
|
},
|
|
872
876
|
]);
|
|
873
877
|
}
|
|
874
|
-
channel.send({
|
|
878
|
+
await channel.send({
|
|
875
879
|
type: 'tool-result',
|
|
876
880
|
toolCallId: input.toolCallId,
|
|
877
881
|
toolName: pending.toolName,
|
|
@@ -882,8 +886,8 @@ export async function resumeAgent(input, channel, params, options) {
|
|
|
882
886
|
const updatedRun = await agentRunState.getRun(run.runId);
|
|
883
887
|
const remaining = updatedRun?.pendingApprovals ?? [];
|
|
884
888
|
if (remaining.length > 0) {
|
|
885
|
-
channel.send({ type: 'done' });
|
|
886
|
-
channel.close();
|
|
889
|
+
await channel.send({ type: 'done' });
|
|
890
|
+
await channel.close();
|
|
887
891
|
return;
|
|
888
892
|
}
|
|
889
893
|
await agentRunState.updateRun(run.runId, { status: 'running' });
|
|
@@ -1002,7 +1006,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
1002
1006
|
});
|
|
1003
1007
|
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
1004
1008
|
await wrappedChannel.send({ type: 'done' });
|
|
1005
|
-
channel.close();
|
|
1009
|
+
await channel.close();
|
|
1006
1010
|
}
|
|
1007
1011
|
catch (err) {
|
|
1008
1012
|
// Same reasoning as the first turn: an interrupt is not a failure, and the
|
|
@@ -1014,14 +1018,14 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
1014
1018
|
if (storage) {
|
|
1015
1019
|
trackInterruptNote(run.threadId, persistOrphanedToolResults(interruptHandle, storage, run.threadId));
|
|
1016
1020
|
}
|
|
1017
|
-
channel.send({
|
|
1021
|
+
await channel.send({
|
|
1018
1022
|
type: 'interrupted',
|
|
1019
1023
|
runId: run.runId,
|
|
1020
1024
|
text: persistingChannel.fullText,
|
|
1021
1025
|
reason: interruption?.reason ?? 'user',
|
|
1022
1026
|
});
|
|
1023
|
-
channel.send({ type: 'done' });
|
|
1024
|
-
channel.close();
|
|
1027
|
+
await channel.send({ type: 'done' });
|
|
1028
|
+
await channel.close();
|
|
1025
1029
|
return;
|
|
1026
1030
|
}
|
|
1027
1031
|
for (const mw of agentMiddlewares) {
|
|
@@ -1040,12 +1044,12 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
1040
1044
|
status: 'failed',
|
|
1041
1045
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1042
1046
|
});
|
|
1043
|
-
channel.send({
|
|
1047
|
+
await channel.send({
|
|
1044
1048
|
type: 'error',
|
|
1045
1049
|
message: err instanceof Error ? err.message : String(err),
|
|
1046
1050
|
});
|
|
1047
|
-
channel.send({ type: 'done' });
|
|
1048
|
-
channel.close();
|
|
1051
|
+
await channel.send({ type: 'done' });
|
|
1052
|
+
await channel.close();
|
|
1049
1053
|
}
|
|
1050
1054
|
finally {
|
|
1051
1055
|
interruptHandle.release();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
|
|
2
2
|
import type { CorePikkuMiddleware, MiddlewareMetadata } from '../../middleware/middleware.types.js';
|
|
3
|
+
import type { AbortScope } from '../../function/abort-scope.js';
|
|
3
4
|
import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
4
5
|
import type { PermissionMetadata } from '../../function/function-meta.types.js';
|
|
5
6
|
import type { AIProviderOptions } from '../../services/agent-runner-service.js';
|
|
@@ -166,7 +167,13 @@ export interface AgentToolDef extends Partial<ApprovalPolicy> {
|
|
|
166
167
|
name: string;
|
|
167
168
|
description: string;
|
|
168
169
|
inputSchema: Record<string, unknown>;
|
|
169
|
-
|
|
170
|
+
/**
|
|
171
|
+
* `options.abortScope` is supplied by the interrupt tracker and must be put
|
|
172
|
+
* on the wire of whatever this runs, so `beginChanges()` can observe it.
|
|
173
|
+
*/
|
|
174
|
+
execute: (input: unknown, options?: {
|
|
175
|
+
abortScope?: AbortScope;
|
|
176
|
+
}) => Promise<unknown>;
|
|
170
177
|
/**
|
|
171
178
|
* Mirrors the pikku function's `readonly` flag. A read that gets interrupted
|
|
172
179
|
* is discarded rather than reported: nothing changed, and by the next turn
|
|
@@ -47,7 +47,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
|
|
|
47
47
|
: 'the default message route';
|
|
48
48
|
logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`);
|
|
49
49
|
// knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
|
|
50
|
-
channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
|
|
50
|
+
await channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
const routeMeta = getRouteMeta(channelConfig.name, routingProperty, routerValue);
|
|
@@ -14,6 +14,10 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
|
|
|
14
14
|
registerOnOpen(callback) {
|
|
15
15
|
this.openCallBack = callback;
|
|
16
16
|
}
|
|
17
|
+
// The base class declares `open(): void`; this override is intentionally
|
|
18
|
+
// async (callers already await it). Widening the exported base signature is
|
|
19
|
+
// a public API change, so the rule is suppressed for this one override.
|
|
20
|
+
// oxlint-disable-next-line typescript/no-misused-promises
|
|
17
21
|
async open() {
|
|
18
22
|
this.getChannel().state = 'open';
|
|
19
23
|
if (this.openCallBack) {
|
|
@@ -39,7 +43,7 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
|
|
|
39
43
|
if (this.getChannel().state === 'closed') {
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
42
|
-
super.close();
|
|
46
|
+
await super.close();
|
|
43
47
|
const results = await Promise.allSettled(this.closeCallbacks.map((cb) => cb()));
|
|
44
48
|
for (const result of results) {
|
|
45
49
|
if (result.status === 'rejected') {
|
|
@@ -98,7 +98,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
98
98
|
catch (e) {
|
|
99
99
|
singletonServices.logger.error(`Error handling onConnect: ${e}`);
|
|
100
100
|
const errorResponse = getErrorResponse(e);
|
|
101
|
-
channel.send({
|
|
101
|
+
await channel.send({
|
|
102
102
|
error: errorResponse?.message ??
|
|
103
103
|
(isProduction() ? 'Internal server error' : e.message),
|
|
104
104
|
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
@@ -124,7 +124,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
124
124
|
catch (e) {
|
|
125
125
|
singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
|
|
126
126
|
const errorResponse = getErrorResponse(e);
|
|
127
|
-
channel.send({
|
|
127
|
+
await channel.send({
|
|
128
128
|
error: errorResponse?.message ??
|
|
129
129
|
(isProduction() ? 'Internal server error' : e.message),
|
|
130
130
|
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
@@ -149,12 +149,14 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
|
|
|
149
149
|
catch (e) {
|
|
150
150
|
singletonServices.logger.error(e);
|
|
151
151
|
const errorResponse = getErrorResponse(e);
|
|
152
|
-
channel.send({
|
|
152
|
+
await channel.send({
|
|
153
153
|
error: errorResponse?.message ??
|
|
154
154
|
(isProduction() ? 'Internal server error' : e.message),
|
|
155
155
|
...(!isProduction() && { errorName: e.constructor?.name }),
|
|
156
156
|
});
|
|
157
|
-
setTimeout(() =>
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
Promise.resolve(channel.close()).catch((closeError) => singletonServices.logger.error(closeError));
|
|
159
|
+
}, 200);
|
|
158
160
|
}
|
|
159
161
|
});
|
|
160
162
|
if (onBinaryMessage) {
|
|
@@ -6,6 +6,7 @@ export declare class LocalEventHubService<Data extends Record<string, any> = {}>
|
|
|
6
6
|
subscribe<T extends keyof Data>(topic: T, channelId: string): void | Promise<void>;
|
|
7
7
|
unsubscribe<T extends keyof Data>(topic: T, channelId: string): void | Promise<void>;
|
|
8
8
|
publish<T extends keyof Data>(topic: T, channelId: string | null, data: Data[T], isBinary?: boolean): void | Promise<void>;
|
|
9
|
+
private deliver;
|
|
9
10
|
onChannelOpened(channelHandler: PikkuChannelHandler): void;
|
|
10
11
|
onChannelClosed(channelId: string): void;
|
|
11
12
|
}
|
|
@@ -17,6 +17,9 @@ export class LocalEventHubService {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
publish(topic, channelId, data, isBinary) {
|
|
20
|
+
return this.deliver(topic, channelId, data, isBinary);
|
|
21
|
+
}
|
|
22
|
+
async deliver(topic, channelId, data, isBinary) {
|
|
20
23
|
const subscribedChannelIds = this.subscriptions.get(topic);
|
|
21
24
|
if (!subscribedChannelIds) {
|
|
22
25
|
return;
|
|
@@ -27,7 +30,7 @@ export class LocalEventHubService {
|
|
|
27
30
|
const channel = this.channels.get(toChannelId);
|
|
28
31
|
if (channel) {
|
|
29
32
|
try {
|
|
30
|
-
channel.send(data, isBinary);
|
|
33
|
+
await channel.send(data, isBinary);
|
|
31
34
|
}
|
|
32
35
|
catch {
|
|
33
36
|
this.onChannelClosed(toChannelId);
|
|
@@ -42,7 +42,7 @@ export async function executeCLIViaChannel({ programName, pikkuWS, args = proces
|
|
|
42
42
|
const renderer = renderers[commandId] || defaultRenderer || defaultJSONRenderer;
|
|
43
43
|
return new Promise((resolve, reject) => {
|
|
44
44
|
const commandRoute = pikkuWS.getRoute('command');
|
|
45
|
-
const responseHandler = (response) => {
|
|
45
|
+
const responseHandler = async (response) => {
|
|
46
46
|
if (response?.action === 'cli-control' &&
|
|
47
47
|
response?.event === 'complete') {
|
|
48
48
|
commandRoute.unsubscribe(commandId, responseHandler);
|
|
@@ -52,7 +52,12 @@ export async function executeCLIViaChannel({ programName, pikkuWS, args = proces
|
|
|
52
52
|
}
|
|
53
53
|
// A CLI channel response is rendered off-process: there are no services
|
|
54
54
|
// to hand the renderer, and every CLI renderer ignores the argument.
|
|
55
|
-
|
|
55
|
+
try {
|
|
56
|
+
await renderer(null, response, undefined);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
reject(error);
|
|
60
|
+
}
|
|
56
61
|
};
|
|
57
62
|
commandRoute.subscribe(commandId, responseHandler);
|
|
58
63
|
pikkuWS.ws.addEventListener('close', () => {
|
|
@@ -35,11 +35,11 @@ export async function executeRawCLIViaChannel({ pikkuWS, args = process.argv.sli
|
|
|
35
35
|
// event loop open.
|
|
36
36
|
const runEnded = new AbortController();
|
|
37
37
|
const approver = approve ?? approverForMode(mode, { signal: runEnded.signal });
|
|
38
|
-
const render = (commandId, data) => {
|
|
38
|
+
const render = async (commandId, data) => {
|
|
39
39
|
const renderer = (commandId ? renderers[commandId] : undefined) ||
|
|
40
40
|
defaultRenderer ||
|
|
41
41
|
defaultJSONRenderer;
|
|
42
|
-
renderer(clientServices, data, undefined);
|
|
42
|
+
await renderer(clientServices, data, undefined);
|
|
43
43
|
};
|
|
44
44
|
return new Promise((resolve) => {
|
|
45
45
|
const commandRoute = pikkuWS.getRoute('command');
|
|
@@ -88,10 +88,14 @@ export async function executeRawCLIViaChannel({ pikkuWS, args = process.argv.sli
|
|
|
88
88
|
// echo, and rendering that would print noise between real output.
|
|
89
89
|
switch (message?.action) {
|
|
90
90
|
case 'cli-output':
|
|
91
|
-
render(message.commandId, message.data)
|
|
91
|
+
render(message.commandId, message.data).catch((e) => {
|
|
92
|
+
console.error(e);
|
|
93
|
+
});
|
|
92
94
|
break;
|
|
93
95
|
case 'cli-result':
|
|
94
|
-
render(message.commandId, message.result)
|
|
96
|
+
render(message.commandId, message.result).catch((e) => {
|
|
97
|
+
console.error(e);
|
|
98
|
+
});
|
|
95
99
|
break;
|
|
96
100
|
case 'cli-help':
|
|
97
101
|
console.log(message.help);
|
|
@@ -232,7 +232,7 @@ const wireWebsocketGateway = (config) => {
|
|
|
232
232
|
senderId: '',
|
|
233
233
|
platform: adapter.name,
|
|
234
234
|
send: async (msg) => {
|
|
235
|
-
wire.channel?.send(msg);
|
|
235
|
+
await wire.channel?.send(msg);
|
|
236
236
|
},
|
|
237
237
|
};
|
|
238
238
|
},
|
|
@@ -249,7 +249,7 @@ const wireWebsocketGateway = (config) => {
|
|
|
249
249
|
senderId: parsed.senderId,
|
|
250
250
|
platform: adapter.name,
|
|
251
251
|
send: async (msg) => {
|
|
252
|
-
wire.channel?.send(msg);
|
|
252
|
+
await wire.channel?.send(msg);
|
|
253
253
|
},
|
|
254
254
|
};
|
|
255
255
|
wire.gateway = gateway;
|
|
@@ -268,7 +268,7 @@ const wireWebsocketGateway = (config) => {
|
|
|
268
268
|
? await runMiddleware(services, wire, gatewayMiddleware, invoke)
|
|
269
269
|
: await invoke();
|
|
270
270
|
if (result && (result.text || result.richContent || result.attachments)) {
|
|
271
|
-
wire.channel?.send(result);
|
|
271
|
+
await wire.channel?.send(result);
|
|
272
272
|
}
|
|
273
273
|
},
|
|
274
274
|
});
|
|
@@ -149,19 +149,19 @@ const executeRoute = async (services, matchedRoute, http, options) => {
|
|
|
149
149
|
const channelRef = channel;
|
|
150
150
|
const channelHandler = {
|
|
151
151
|
getChannel: () => channelRef,
|
|
152
|
-
send: (data, isBinary) => {
|
|
152
|
+
send: async (data, isBinary) => {
|
|
153
153
|
if (isBinary)
|
|
154
|
-
channelRef.sendBinary(data);
|
|
154
|
+
await channelRef.sendBinary(data);
|
|
155
155
|
else
|
|
156
|
-
channelRef.send(data);
|
|
156
|
+
await channelRef.send(data);
|
|
157
157
|
},
|
|
158
158
|
sendBinary: (data) => channelRef.sendBinary(data),
|
|
159
159
|
};
|
|
160
160
|
singletonServices.eventHub.onChannelOpened(channelHandler);
|
|
161
161
|
const originalClose = channel.close;
|
|
162
|
-
channel.close = () => {
|
|
162
|
+
channel.close = async () => {
|
|
163
163
|
singletonServices.eventHub.onChannelClosed(channelId);
|
|
164
|
-
originalClose();
|
|
164
|
+
await originalClose();
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
}
|
|
@@ -201,7 +201,7 @@ const executeRoute = async (services, matchedRoute, http, options) => {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
catch { }
|
|
204
|
-
channel?.close();
|
|
204
|
+
await channel?.close();
|
|
205
205
|
return { result };
|
|
206
206
|
}
|
|
207
207
|
throw e;
|
|
@@ -14,6 +14,9 @@ export declare class RemoteAddonRequestError extends PikkuError {
|
|
|
14
14
|
readonly httpStatus: number;
|
|
15
15
|
constructor(namespace: string, fnName: string, status: number, detail: string);
|
|
16
16
|
}
|
|
17
|
+
export declare class AddonNotUsedError extends PikkuError {
|
|
18
|
+
constructor(caller: string, target: string);
|
|
19
|
+
}
|
|
17
20
|
export declare const resolveNamespace: (namespacedFunction: string) => ResolvedFunction | null;
|
|
18
21
|
export declare class ContextAwareRPCService {
|
|
19
22
|
private services;
|
|
@@ -26,6 +29,7 @@ export declare class ContextAwareRPCService {
|
|
|
26
29
|
}, packageName?: string | null);
|
|
27
30
|
rpcExposed(funcName: string, data: any): Promise<any>;
|
|
28
31
|
rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
|
|
32
|
+
private mapUsedAddon;
|
|
29
33
|
/**
|
|
30
34
|
* Resolution half of an addon RPC call: never executes anything, so an
|
|
31
35
|
* `RPCNotFoundError` thrown by the function itself can never be mistaken for
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { addonCallerOf, addonMayReach } from '../addon/addon-reach.js';
|
|
1
2
|
import { forwardStepName, isCompensationStepName, } from '../../function/compensation-name.js';
|
|
2
3
|
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
3
4
|
import { addonInstanceForNamespace } from '../addon/addon-runner.js';
|
|
@@ -43,6 +44,15 @@ addError(RemoteAddonRequestError, {
|
|
|
43
44
|
status: 502,
|
|
44
45
|
message: 'Remote addon request failed.',
|
|
45
46
|
});
|
|
47
|
+
export class AddonNotUsedError extends PikkuError {
|
|
48
|
+
constructor(caller, target) {
|
|
49
|
+
super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
addError(AddonNotUsedError, {
|
|
53
|
+
status: 403,
|
|
54
|
+
message: 'Addon called another addon it does not use.',
|
|
55
|
+
});
|
|
46
56
|
export const resolveNamespace = (namespacedFunction) => {
|
|
47
57
|
const colonIndex = namespacedFunction.indexOf(':');
|
|
48
58
|
if (colonIndex === -1) {
|
|
@@ -68,6 +78,7 @@ const resolvePikkuFunction = (rpcName, packageName = null) => {
|
|
|
68
78
|
if (pkgMeta) {
|
|
69
79
|
return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName };
|
|
70
80
|
}
|
|
81
|
+
throw new RPCNotFoundError(rpcName);
|
|
71
82
|
}
|
|
72
83
|
const rpc = pikkuState(null, 'rpc', 'meta');
|
|
73
84
|
let rpcMeta = rpc[rpcName];
|
|
@@ -119,6 +130,10 @@ export class ContextAwareRPCService {
|
|
|
119
130
|
this.packageName = packageName;
|
|
120
131
|
}
|
|
121
132
|
async rpcExposed(funcName, data) {
|
|
133
|
+
const addonCaller = addonCallerOf(this.wire);
|
|
134
|
+
if (addonCaller && !addonMayReach(addonCaller, funcName)) {
|
|
135
|
+
throw new AddonNotUsedError(addonCaller, funcName);
|
|
136
|
+
}
|
|
122
137
|
let functionMeta;
|
|
123
138
|
let resolvedAddon = null;
|
|
124
139
|
if (funcName.includes(':')) {
|
|
@@ -159,6 +174,7 @@ export class ContextAwareRPCService {
|
|
|
159
174
|
...this.wire,
|
|
160
175
|
};
|
|
161
176
|
if (funcName.includes(':')) {
|
|
177
|
+
funcName = this.mapUsedAddon(funcName);
|
|
162
178
|
const addonCall = this.resolveAddonFunction(funcName);
|
|
163
179
|
if (addonCall !== NOT_RESOLVED) {
|
|
164
180
|
return await this.executeAddonFunction(addonCall, data, updatedWire);
|
|
@@ -170,7 +186,7 @@ export class ContextAwareRPCService {
|
|
|
170
186
|
}
|
|
171
187
|
catch (e) {
|
|
172
188
|
if (e instanceof RPCNotFoundError) {
|
|
173
|
-
if (this.services.deploymentService) {
|
|
189
|
+
if (!this.packageName && this.services.deploymentService) {
|
|
174
190
|
const session = await resolveWireSession(this.wire);
|
|
175
191
|
return this.services.deploymentService.invoke(funcName, data, session, this.wire.traceId);
|
|
176
192
|
}
|
|
@@ -189,6 +205,20 @@ export class ContextAwareRPCService {
|
|
|
189
205
|
addonInstance,
|
|
190
206
|
});
|
|
191
207
|
}
|
|
208
|
+
mapUsedAddon(funcName) {
|
|
209
|
+
const caller = this.wire.addonNamespace;
|
|
210
|
+
if (!caller)
|
|
211
|
+
return funcName;
|
|
212
|
+
const colon = funcName.indexOf(':');
|
|
213
|
+
const prefix = funcName.slice(0, colon);
|
|
214
|
+
if (prefix === caller)
|
|
215
|
+
return funcName;
|
|
216
|
+
const uses = pikkuState(null, 'addons', 'packages').get(caller)?.uses;
|
|
217
|
+
const mapped = uses?.[prefix];
|
|
218
|
+
if (mapped)
|
|
219
|
+
return `${mapped}${funcName.slice(colon)}`;
|
|
220
|
+
throw new AddonNotUsedError(caller, funcName);
|
|
221
|
+
}
|
|
192
222
|
/**
|
|
193
223
|
* Resolution half of an addon RPC call: never executes anything, so an
|
|
194
224
|
* `RPCNotFoundError` thrown by the function itself can never be mistaken for
|
|
@@ -243,6 +273,9 @@ export class ContextAwareRPCService {
|
|
|
243
273
|
credentialGrants: resolved.addonConfig?.credentialGrants,
|
|
244
274
|
globalSecrets: resolved.addonConfig?.globalSecrets,
|
|
245
275
|
globalCredentials: resolved.addonConfig?.globalCredentials,
|
|
276
|
+
contentBucket: resolved.addonConfig?.contentBucket,
|
|
277
|
+
contentGrants: resolved.addonConfig?.contentGrants,
|
|
278
|
+
globalContent: resolved.addonConfig?.globalContent,
|
|
246
279
|
},
|
|
247
280
|
};
|
|
248
281
|
}
|
|
@@ -305,6 +338,7 @@ export class ContextAwareRPCService {
|
|
|
305
338
|
...wire,
|
|
306
339
|
};
|
|
307
340
|
if (rpcName.includes(':')) {
|
|
341
|
+
rpcName = this.mapUsedAddon(rpcName);
|
|
308
342
|
const addonCall = this.resolveAddonFunction(rpcName);
|
|
309
343
|
if (addonCall !== NOT_RESOLVED) {
|
|
310
344
|
return await this.executeAddonFunction(addonCall, data, mergedWire);
|
|
@@ -315,7 +349,9 @@ export class ContextAwareRPCService {
|
|
|
315
349
|
resolved = resolvePikkuFunction(rpcName, this.packageName);
|
|
316
350
|
}
|
|
317
351
|
catch (e) {
|
|
318
|
-
if (e instanceof RPCNotFoundError &&
|
|
352
|
+
if (e instanceof RPCNotFoundError &&
|
|
353
|
+
!this.packageName &&
|
|
354
|
+
this.services.deploymentService) {
|
|
319
355
|
const session = await resolveWireSession(mergedWire);
|
|
320
356
|
return this.services.deploymentService.invoke(rpcName, data, session, mergedWire.traceId);
|
|
321
357
|
}
|
|
@@ -337,6 +373,9 @@ export class ContextAwareRPCService {
|
|
|
337
373
|
if (!this.services.workflowService) {
|
|
338
374
|
throw new Error('WorkflowService service not available');
|
|
339
375
|
}
|
|
376
|
+
if (workflowName.includes(':')) {
|
|
377
|
+
workflowName = this.mapUsedAddon(workflowName);
|
|
378
|
+
}
|
|
340
379
|
const parentRunId = this.wire.workflowStep?.runId ?? this.wire.graph?.runId;
|
|
341
380
|
const wire = options?.wire ?? {
|
|
342
381
|
type: this.wire.wireType ?? 'unknown',
|
|
@@ -59,6 +59,12 @@ export interface ResolvedFunction {
|
|
|
59
59
|
globalSecrets?: string;
|
|
60
60
|
/** Set by the consuming app: hand this instance the unscoped `CredentialService` */
|
|
61
61
|
globalCredentials?: string;
|
|
62
|
+
/** Set by the consuming app: the content folder this instance owns */
|
|
63
|
+
contentBucket?: string;
|
|
64
|
+
/** Set by the consuming app: paths outside its folder this instance may read or write */
|
|
65
|
+
contentGrants?: Record<string, 'read' | 'write'>;
|
|
66
|
+
/** Set by the consuming app: hand this instance the unscoped `ContentService` */
|
|
67
|
+
globalContent?: string;
|
|
62
68
|
/** Set by `wireRemoteAddon`: dispatch this namespace's RPCs over HTTP */
|
|
63
69
|
remote?: boolean;
|
|
64
70
|
serverUrl?: string | ((services: any) => string | Promise<string>);
|
|
@@ -43,7 +43,7 @@ export async function runScheduledTask({ name, session, traceId, }) {
|
|
|
43
43
|
const meta = pikkuState(null, 'scheduler', 'meta')[name];
|
|
44
44
|
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
45
45
|
if (session) {
|
|
46
|
-
userSession.set(session);
|
|
46
|
+
await userSession.set(session);
|
|
47
47
|
}
|
|
48
48
|
if (!task) {
|
|
49
49
|
throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`);
|
|
@@ -30,3 +30,22 @@ import type { CoreScopes } from './scope.types.js';
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export declare const defineScope: (_config: CoreScopes) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Declares scopes by id without describing them, so an app can grant a scope
|
|
35
|
+
* (`defineSystemRole`, a function's `scopes`) whose full tree belongs to an
|
|
36
|
+
* addon it has not wired. `wireAddon({ scopes })` does not declare anything: it
|
|
37
|
+
* only requires the scope of the addon's functions, and the tree itself arrives
|
|
38
|
+
* with the addon's metadata. Declaring by id lets an app grant
|
|
39
|
+
* `pikku:console` without wiring `@pikku/addon-console`, which would otherwise
|
|
40
|
+
* be the only way to make that grant valid.
|
|
41
|
+
*
|
|
42
|
+
* Each id declares that node and its ancestors. When the addon IS wired, its
|
|
43
|
+
* own description wins and this call adds nothing it does not already have; the
|
|
44
|
+
* two never conflict, unlike a second `defineScope` for the same root.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* declareScopes(['pikku:console'])
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const declareScopes: (_ids: string[]) => void;
|