@pikku/core 0.12.137 → 0.12.138
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 +31 -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/auth-apikey.js +1 -1
- package/dist/middleware/auth-bearer.js +1 -1
- package/dist/middleware/auth-cookie.js +1 -1
- package/dist/schema.js +3 -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 +0 -1
- package/dist/services/index.js +0 -1
- 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/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/utils/hmac.d.ts +25 -10
- package/dist/utils/hmac.js +202 -24
- 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 +21 -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/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 +1 -1
- package/dist/wirings/scope/index.js +1 -1
- 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 +6 -3
|
@@ -247,7 +247,7 @@ export function createScopedChannel(parent, agentName, session) {
|
|
|
247
247
|
},
|
|
248
248
|
close: () => { },
|
|
249
249
|
sendBinary: (data) => parent.sendBinary(data),
|
|
250
|
-
send: (event) => {
|
|
250
|
+
send: async (event) => {
|
|
251
251
|
if (event.type === 'done')
|
|
252
252
|
return;
|
|
253
253
|
if (event.type === 'approval-request') {
|
|
@@ -267,10 +267,14 @@ export function createScopedChannel(parent, agentName, session) {
|
|
|
267
267
|
event.type === 'tool-result' ||
|
|
268
268
|
event.type === 'usage' ||
|
|
269
269
|
event.type === 'error') {
|
|
270
|
-
parent.send({
|
|
270
|
+
await parent.send({
|
|
271
|
+
...event,
|
|
272
|
+
agent: agentName,
|
|
273
|
+
session,
|
|
274
|
+
});
|
|
271
275
|
}
|
|
272
276
|
else {
|
|
273
|
-
parent.send(event);
|
|
277
|
+
await parent.send(event);
|
|
274
278
|
}
|
|
275
279
|
},
|
|
276
280
|
setState: (s) => parent.setState(s),
|
|
@@ -402,10 +406,12 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
402
406
|
needsApproval: needsApproval || undefined,
|
|
403
407
|
approvalDescriptionFn,
|
|
404
408
|
readonly: fnMeta?.readonly || undefined,
|
|
405
|
-
execute: async (toolInput) => {
|
|
409
|
+
execute: async (toolInput, options) => {
|
|
406
410
|
const wire = params.sessionService
|
|
407
411
|
? { ...createMiddlewareSessionWireProps(params.sessionService) }
|
|
408
412
|
: {};
|
|
413
|
+
if (options?.abortScope)
|
|
414
|
+
wire.abortScope = options.abortScope;
|
|
409
415
|
const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
|
|
410
416
|
return rpcService.rpc(toolName, toolInput);
|
|
411
417
|
},
|
|
@@ -457,7 +463,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
457
463
|
streamContext.delegateState.delegated = true;
|
|
458
464
|
}
|
|
459
465
|
const { channel } = streamContext;
|
|
460
|
-
channel.send({
|
|
466
|
+
await channel.send({
|
|
461
467
|
type: 'agent-call',
|
|
462
468
|
agentName: subAgentName,
|
|
463
469
|
session,
|
|
@@ -468,11 +474,11 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
468
474
|
? subChannel
|
|
469
475
|
: {
|
|
470
476
|
...subChannel,
|
|
471
|
-
send: (event) => {
|
|
477
|
+
send: async (event) => {
|
|
472
478
|
if (event.type === 'text-delta' ||
|
|
473
479
|
event.type === 'reasoning-delta')
|
|
474
480
|
return;
|
|
475
|
-
subChannel.send(event);
|
|
481
|
+
await subChannel.send(event);
|
|
476
482
|
},
|
|
477
483
|
};
|
|
478
484
|
const resultText = await streamAgent(subAgentName, buildSubAgentRunInput(message, threadId, resourceId, parentContext), effectiveChannel, params, agentSessionMap, streamContext.options);
|
|
@@ -486,7 +492,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
486
492
|
subApprovals: subChannel.approvals,
|
|
487
493
|
};
|
|
488
494
|
}
|
|
489
|
-
channel.send({
|
|
495
|
+
await channel.send({
|
|
490
496
|
type: 'agent-result',
|
|
491
497
|
agentName: subAgentName,
|
|
492
498
|
session,
|
|
@@ -544,7 +550,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
544
550
|
name: workflowName,
|
|
545
551
|
description: wfMeta.description || workflowName,
|
|
546
552
|
inputSchema,
|
|
547
|
-
execute: async (toolInput) => {
|
|
553
|
+
execute: async (toolInput, options) => {
|
|
548
554
|
const workflowService = singletonServices.workflowService;
|
|
549
555
|
if (!workflowService) {
|
|
550
556
|
throw new Error(`workflowService is not configured — cannot run workflow tool '${workflowName}'`);
|
|
@@ -552,6 +558,8 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
552
558
|
const wire = params.sessionService
|
|
553
559
|
? { ...createMiddlewareSessionWireProps(params.sessionService) }
|
|
554
560
|
: {};
|
|
561
|
+
if (options?.abortScope)
|
|
562
|
+
wire.abortScope = options.abortScope;
|
|
555
563
|
const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
|
|
556
564
|
return workflowService.runToCompletion(workflowName, toolInput, rpcService, { wire: { type: 'internal' } });
|
|
557
565
|
},
|
|
@@ -560,9 +568,9 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
560
568
|
}
|
|
561
569
|
for (const tool of tools) {
|
|
562
570
|
const originalExecute = tool.execute;
|
|
563
|
-
tool.execute = async (toolInput) => {
|
|
571
|
+
tool.execute = async (toolInput, options) => {
|
|
564
572
|
try {
|
|
565
|
-
return await originalExecute(toolInput);
|
|
573
|
+
return await originalExecute(toolInput, options);
|
|
566
574
|
}
|
|
567
575
|
catch (err) {
|
|
568
576
|
if (err?.payload?.error === 'missing_credential') {
|
|
@@ -581,7 +589,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
581
589
|
if (hasToolHooks) {
|
|
582
590
|
for (const tool of tools) {
|
|
583
591
|
const originalExecute = tool.execute;
|
|
584
|
-
tool.execute = async (toolInput) => {
|
|
592
|
+
tool.execute = async (toolInput, options) => {
|
|
585
593
|
const toolCallId = randomUUID();
|
|
586
594
|
let args = (toolInput ?? {});
|
|
587
595
|
for (const mw of agentMiddlewares) {
|
|
@@ -600,7 +608,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
600
608
|
let result;
|
|
601
609
|
let execError;
|
|
602
610
|
try {
|
|
603
|
-
result = await originalExecute(args);
|
|
611
|
+
result = await originalExecute(args, options);
|
|
604
612
|
}
|
|
605
613
|
catch (err) {
|
|
606
614
|
execError = err;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -29,3 +29,22 @@
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
export const defineScope = (_config) => { };
|
|
32
|
+
/**
|
|
33
|
+
* Declares scopes by id without describing them, so an app can grant a scope
|
|
34
|
+
* (`defineSystemRole`, a function's `scopes`) whose full tree belongs to an
|
|
35
|
+
* addon it has not wired. `wireAddon({ scopes })` does not declare anything: it
|
|
36
|
+
* only requires the scope of the addon's functions, and the tree itself arrives
|
|
37
|
+
* with the addon's metadata. Declaring by id lets an app grant
|
|
38
|
+
* `pikku:console` without wiring `@pikku/addon-console`, which would otherwise
|
|
39
|
+
* be the only way to make that grant valid.
|
|
40
|
+
*
|
|
41
|
+
* Each id declares that node and its ancestors. When the addon IS wired, its
|
|
42
|
+
* own description wins and this call adds nothing it does not already have; the
|
|
43
|
+
* two never conflict, unlike a second `defineScope` for the same root.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* declareScopes(['pikku:console'])
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const declareScopes = (_ids) => { };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineScope } from './define-scope.js';
|
|
1
|
+
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
3
|
export type { CoreScopes, CoreScopeNode, FlatScope, ScopeDefinitionMeta, ScopeDefinitionsMeta, ScopeDefinitions, ScopeNodeMeta, ScopeOrigin, } from './scope.types.js';
|
|
4
4
|
export { hasScopes, verifyScopes } from '../../scopes.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { defineScope } from './define-scope.js';
|
|
1
|
+
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
3
|
export { hasScopes, verifyScopes } from '../../scopes.js';
|