@vincemakes/kiso-core 0.1.6 → 0.1.7
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/dist/kernel/loop.js +87 -63
- package/package.json +2 -2
package/dist/kernel/loop.js
CHANGED
|
@@ -588,6 +588,70 @@ async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, res
|
|
|
588
588
|
// all. Checked again here, after any permission path.
|
|
589
589
|
if (signal?.aborted)
|
|
590
590
|
throw ABORTED;
|
|
591
|
+
/**
|
|
592
|
+
* The human approval pause (Phase D / 裁决 A): register the resolver
|
|
593
|
+
* BEFORE announcing the request (a consumer that answers the moment it
|
|
594
|
+
* sees the event must find the resolver already waiting — no deadlock
|
|
595
|
+
* between yield and await), persist the request, yield it, await the
|
|
596
|
+
* human's decision — abortable (an abort during the wait ends the run;
|
|
597
|
+
* a verdict given in the same instant is still recorded exactly once) —
|
|
598
|
+
* then persist and yield the decision. Returns the human's verdict.
|
|
599
|
+
*/
|
|
600
|
+
async function* awaitHumanApproval(decisionId) {
|
|
601
|
+
const pendingDecision = resolveApproval !== undefined
|
|
602
|
+
? resolveApproval(decisionId)
|
|
603
|
+
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
604
|
+
const requested = log.append({
|
|
605
|
+
type: "permission_requested",
|
|
606
|
+
decisionId,
|
|
607
|
+
callId: call.callId,
|
|
608
|
+
name: call.name,
|
|
609
|
+
input: payload.input,
|
|
610
|
+
});
|
|
611
|
+
if (hooks.onPause)
|
|
612
|
+
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
|
613
|
+
yield requested;
|
|
614
|
+
// Area 4: the pause is abortable — a cancel during the human's wait
|
|
615
|
+
// ends the run now; the request stays durable and pending.
|
|
616
|
+
let finalDecision;
|
|
617
|
+
try {
|
|
618
|
+
finalDecision = await raceAbort(pendingDecision, signal);
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err === ABORTED) {
|
|
622
|
+
// 第四轮(对抗): the human may have answered in the same instant
|
|
623
|
+
// the abort landed — a CONSUMED verdict must be recorded
|
|
624
|
+
// (exactly once), never lost; the abort then ends the run with
|
|
625
|
+
// its honest aborted terminal.
|
|
626
|
+
const verdict = resolveApprovalVerdict?.(decisionId);
|
|
627
|
+
if (verdict !== undefined) {
|
|
628
|
+
yield log.append({
|
|
629
|
+
type: "permission_decided",
|
|
630
|
+
decisionId,
|
|
631
|
+
callId: call.callId,
|
|
632
|
+
decision: verdict ? "approved" : "denied",
|
|
633
|
+
...(verdict ? {} : { reason: "denied by user" }),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
639
|
+
// The approval channel (session.approve) persists the decision
|
|
640
|
+
// write-ahead BEFORE waking the resolver (Area 2): if it already
|
|
641
|
+
// landed in the log, this is the same decision, not a duplicate.
|
|
642
|
+
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === decisionId) ??
|
|
643
|
+
log.append({
|
|
644
|
+
type: "permission_decided",
|
|
645
|
+
decisionId,
|
|
646
|
+
callId: call.callId, // binds the decision to the invocation (B 组)
|
|
647
|
+
decision: finalDecision.action === "allow" ? "approved" : "denied",
|
|
648
|
+
...(finalDecision.action === "deny" && finalDecision.reason !== undefined
|
|
649
|
+
? { reason: finalDecision.reason }
|
|
650
|
+
: {}),
|
|
651
|
+
});
|
|
652
|
+
yield decided;
|
|
653
|
+
return finalDecision;
|
|
654
|
+
}
|
|
591
655
|
// ── E1: the extension policy chain, decided BEFORE the human flow ─────
|
|
592
656
|
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
593
657
|
// chain never re-runs when its verdict is already in the log (同构
|
|
@@ -651,77 +715,37 @@ async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, res
|
|
|
651
715
|
yield emitResult(denialResult(durable.reason ?? "denied"));
|
|
652
716
|
return;
|
|
653
717
|
}
|
|
654
|
-
if (chainVerdict?.action === "ask"
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
718
|
+
if (chainVerdict?.action === "ask") {
|
|
719
|
+
// 裁决 A (E1 ask 语义修正): an ask means "a HUMAN must decide" — it
|
|
720
|
+
// routes DIRECTLY to the human approval pause, never through
|
|
721
|
+
// onPreTool: a static automated policy (e.g. the CLI's default deny
|
|
722
|
+
// for unknown tools) must not answer for the human. No approval
|
|
723
|
+
// channel configured → an honest denial (judged by resolveApproval,
|
|
724
|
+
// not by the hook's presence).
|
|
725
|
+
if (resolveApproval === undefined) {
|
|
726
|
+
yield emitResult(denialResult("a policy asked for a human decision, but no approval flow is configured"));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const decisionId = `d-${log.lastSeq + 1}`;
|
|
730
|
+
const finalDecision = yield* awaitHumanApproval(decisionId);
|
|
731
|
+
if (finalDecision.action !== "allow") {
|
|
732
|
+
yield emitResult(denialResult(finalDecision.reason ?? "denied"));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
659
735
|
}
|
|
660
736
|
// Permission negotiation — defer is a REAL pause (Phase D). C 组: the
|
|
661
737
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
662
|
-
// abort), and the signal is re-checked after it returns.
|
|
663
|
-
// the policy chain
|
|
664
|
-
|
|
738
|
+
// abort), and the signal is re-checked after it returns. Runs only when
|
|
739
|
+
// the policy chain did not run at all (裁决 A: an ask was already
|
|
740
|
+
// resolved by the human pause above — the static hook never speaks for
|
|
741
|
+
// it, and a durable decision already spoke for the call).
|
|
742
|
+
if (durable === undefined && chainVerdict === undefined && hooks.onPreTool) {
|
|
665
743
|
const decision = await raceAbort(hooks.onPreTool(payload, ctx), signal);
|
|
666
744
|
if (signal?.aborted)
|
|
667
745
|
throw ABORTED;
|
|
668
746
|
if (decision.action === "defer") {
|
|
669
747
|
const decisionId = `d-${log.lastSeq + 1}`;
|
|
670
|
-
|
|
671
|
-
// that answers the request the moment it sees it must find the
|
|
672
|
-
// resolver already waiting (no deadlock between yield and await).
|
|
673
|
-
const pendingDecision = resolveApproval !== undefined
|
|
674
|
-
? resolveApproval(decisionId)
|
|
675
|
-
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
676
|
-
const requested = log.append({
|
|
677
|
-
type: "permission_requested",
|
|
678
|
-
decisionId,
|
|
679
|
-
callId: call.callId,
|
|
680
|
-
name: call.name,
|
|
681
|
-
input: payload.input,
|
|
682
|
-
});
|
|
683
|
-
if (hooks.onPause)
|
|
684
|
-
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
|
685
|
-
yield requested;
|
|
686
|
-
// Area 4: the pause is abortable — a cancel during the human's
|
|
687
|
-
// wait ends the run now; the request stays durable and pending.
|
|
688
|
-
let finalDecision;
|
|
689
|
-
try {
|
|
690
|
-
finalDecision = await raceAbort(pendingDecision, signal);
|
|
691
|
-
}
|
|
692
|
-
catch (err) {
|
|
693
|
-
if (err === ABORTED) {
|
|
694
|
-
// 第四轮(对抗): the human may have answered in the same
|
|
695
|
-
// instant the abort landed — a CONSUMED verdict must be
|
|
696
|
-
// recorded (exactly once), never lost; the abort then
|
|
697
|
-
// ends the run with its honest aborted terminal.
|
|
698
|
-
const verdict = resolveApprovalVerdict?.(decisionId);
|
|
699
|
-
if (verdict !== undefined) {
|
|
700
|
-
yield log.append({
|
|
701
|
-
type: "permission_decided",
|
|
702
|
-
decisionId,
|
|
703
|
-
callId: call.callId,
|
|
704
|
-
decision: verdict ? "approved" : "denied",
|
|
705
|
-
...(verdict ? {} : { reason: "denied by user" }),
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
throw err;
|
|
710
|
-
}
|
|
711
|
-
// The approval channel (session.approve) persists the decision
|
|
712
|
-
// write-ahead BEFORE waking the resolver (Area 2): if it already
|
|
713
|
-
// landed in the log, this is the same decision, not a duplicate.
|
|
714
|
-
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === decisionId) ??
|
|
715
|
-
log.append({
|
|
716
|
-
type: "permission_decided",
|
|
717
|
-
decisionId,
|
|
718
|
-
callId: call.callId, // binds the decision to the invocation (B 组)
|
|
719
|
-
decision: finalDecision.action === "allow" ? "approved" : "denied",
|
|
720
|
-
...(finalDecision.action === "deny" && finalDecision.reason !== undefined
|
|
721
|
-
? { reason: finalDecision.reason }
|
|
722
|
-
: {}),
|
|
723
|
-
});
|
|
724
|
-
yield decided;
|
|
748
|
+
const finalDecision = yield* awaitHumanApproval(decisionId);
|
|
725
749
|
if (finalDecision.action !== "allow") {
|
|
726
750
|
yield emitResult(denialResult(finalDecision.reason ?? "denied"));
|
|
727
751
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "kiso(基礎) core — protocol, event log, loop, hooks, modes, permissions, compaction, delivery truth. The 2,000-line kernel at the bottom of the kiso framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openai"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.7",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|