@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.
Files changed (2) hide show
  1. package/dist/kernel/loop.js +87 -63
  2. package/package.json +2 -2
@@ -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" && hooks.onPreTool === undefined) {
655
- // No human flow exists the ask degrades to an honest denial, never
656
- // an unasked execution (mirrors the defer-without-channel path).
657
- yield emitResult(denialResult("a policy asked for a human decision, but no approval flow is configured"));
658
- return;
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. Skipped when
663
- // the policy chain already decided (durable or all-allow).
664
- if (durable === undefined && chainVerdict?.action !== "allow" && hooks.onPreTool) {
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
- // Register the resolver BEFORE announcing the pause: a consumer
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.6",
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.6",
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"