@polderlabs/bizar-omp 0.7.7 → 0.7.9
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/README.md +15 -11
- package/dist/cli/agents-ink.d.ts +9 -0
- package/dist/cli/agents-ink.d.ts.map +1 -1
- package/dist/cli/agents-ink.js +57 -15
- package/dist/cli/agents-ink.js.map +1 -1
- package/dist/cli/agents.d.ts +1 -1
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +23 -4
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/doctor.d.ts +4 -4
- package/dist/cli/omb-update.d.ts.map +1 -1
- package/dist/cli/omb-update.js +17 -2
- package/dist/cli/omb-update.js.map +1 -1
- package/dist/core/acceptance.d.ts.map +1 -1
- package/dist/core/acceptance.js +22 -2
- package/dist/core/acceptance.js.map +1 -1
- package/dist/core/transitions.d.ts.map +1 -1
- package/dist/core/transitions.js +17 -1
- package/dist/core/transitions.js.map +1 -1
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +15 -1
- package/dist/core/types.js.map +1 -1
- package/dist/extension.d.ts +2 -0
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +220 -79
- package/dist/extension.js.map +1 -1
- package/dist/omp/compatibility.d.ts +5 -5
- package/dist/omp/compatibility.d.ts.map +1 -1
- package/dist/omp/compatibility.js +2 -2
- package/dist/omp/compatibility.js.map +1 -1
- package/dist/omp/daemon-service.d.ts +2 -0
- package/dist/omp/daemon-service.d.ts.map +1 -1
- package/dist/omp/daemon-service.js +38 -7
- package/dist/omp/daemon-service.js.map +1 -1
- package/dist/omp/lifecycle.d.ts +1 -1
- package/dist/omp/lifecycle.d.ts.map +1 -1
- package/dist/omp/lifecycle.js +2 -1
- package/dist/omp/lifecycle.js.map +1 -1
- package/dist/omp/omb-sessions.d.ts +54 -5
- package/dist/omp/omb-sessions.d.ts.map +1 -1
- package/dist/omp/omb-sessions.js +167 -22
- package/dist/omp/omb-sessions.js.map +1 -1
- package/dist/omp/omb-settings.d.ts +9 -0
- package/dist/omp/omb-settings.d.ts.map +1 -1
- package/dist/omp/omb-settings.js +7 -0
- package/dist/omp/omb-settings.js.map +1 -1
- package/dist/omp/terminal-title.d.ts +70 -0
- package/dist/omp/terminal-title.d.ts.map +1 -0
- package/dist/omp/terminal-title.js +123 -0
- package/dist/omp/terminal-title.js.map +1 -0
- package/dist/omp/tmux.d.ts +69 -0
- package/dist/omp/tmux.d.ts.map +1 -1
- package/dist/omp/tmux.js +193 -14
- package/dist/omp/tmux.js.map +1 -1
- package/dist/storage/journal.d.ts +1 -1
- package/dist/storage/journal.d.ts.map +1 -1
- package/dist/storage/journal.js +2 -1
- package/dist/storage/journal.js.map +1 -1
- package/dist/tools/workflow.d.ts.map +1 -1
- package/dist/tools/workflow.js +11 -1
- package/dist/tools/workflow.js.map +1 -1
- package/docs/compatibility/baseline.json +8 -7
- package/docs/compatibility/supported-surfaces.json +2 -1
- package/docs/development/ci-release-implementation-guide.md +3 -3
- package/docs/releases/0.7.8.md +21 -0
- package/docs/releases/0.7.9.md +18 -0
- package/docs/releases/npm-publishing.md +12 -12
- package/docs/releases/support-matrix.md +1 -1
- package/package.json +1 -1
- package/skills/omp-native-development/SKILL.md +1 -1
- package/skills/omp-native-development/references/accuracy-and-versioning.md +1 -1
- package/skills/omp-native-development/references/source-manifest.json +3 -3
package/dist/extension.js
CHANGED
|
@@ -20,10 +20,14 @@ import { repositoryIdentity, statePath, workingTreeDigest } from "./storage/repo
|
|
|
20
20
|
import { Journal } from "./storage/journal.js";
|
|
21
21
|
import { nativeSessionReference } from "./omp/context.js";
|
|
22
22
|
import { findOnPath } from "./omp/tmux.js";
|
|
23
|
-
import { acquireOmpSessionLease, indexOmbSessionFile, isOmbSessionName, sameOmpSessionFile } from "./omp/omb-sessions.js";
|
|
23
|
+
import { acquireOmpSessionLease, clearOmbSessionReference, indexOmbSessionFile, isOmbSessionName, readOmbJournalNativeId, sameOmpSessionFile } from "./omp/omb-sessions.js";
|
|
24
|
+
import { clearOmbHostGeneration, rebindOmbHostGeneration } from "./omp/tmux.js";
|
|
25
|
+
import { createSessionTitleController, resolveSessionTitleLabel } from "./omp/terminal-title.js";
|
|
24
26
|
import { bindSession } from "./omp/lifecycle.js";
|
|
25
27
|
import { BIZAR_MODEL_ROLE_AGENT_PROMPT, BIZAR_MODEL_ROLE_TOOL, BIZAR_MODEL_ROLES, BIZAR_MODEL_TAGS, buildAgentEvaluationRequest, clipOperatorNotice, commitRolePlan, diffCatalog, formatCatalogDelta, formatRoleAssignmentPlan, planBizarModelRoles, planFromRecommendations, recordOf, reviewRoleRecommendations, roleAssignmentRoute, snapshotCatalog, isOpaqueOmniRouteCombo, omniRouteFailureGuidance, } from "./omp/model-role-assignment.js";
|
|
28
|
+
import { AGENT_ROLES } from "./omp/model-roles.js";
|
|
26
29
|
import { loadOmbSettings, recordCatalogObservation } from "./omp/omb-settings.js";
|
|
30
|
+
import { isRunPhaseActive } from "./core/types.js";
|
|
27
31
|
const objectiveSchema = (z) => z.object({
|
|
28
32
|
objective: z.string(),
|
|
29
33
|
nonGoals: z.array(z.string()),
|
|
@@ -81,6 +85,10 @@ function objectiveOf(value) {
|
|
|
81
85
|
checkSpecs: raw.checkSpecs,
|
|
82
86
|
reviewSpec: raw.reviewSpec,
|
|
83
87
|
policyRevision: raw.policyRevision,
|
|
88
|
+
// `integrate()` is fail-closed without an admitted scope, so dropping this
|
|
89
|
+
// here made controlled integration unreachable through the public tool even
|
|
90
|
+
// when the caller supplied one.
|
|
91
|
+
integrationScope: raw.integrationScope,
|
|
84
92
|
authorizationRef: raw.authorizationRef,
|
|
85
93
|
revision: raw.revision,
|
|
86
94
|
});
|
|
@@ -97,6 +105,14 @@ function bindingFor(ctx) {
|
|
|
97
105
|
}
|
|
98
106
|
return bindSession(new Journal(path), ctx);
|
|
99
107
|
}
|
|
108
|
+
/** Readable tab label for the current session; see `resolveSessionTitleLabel`. */
|
|
109
|
+
function sessionTitleLabel(ctx) {
|
|
110
|
+
return resolveSessionTitleLabel({
|
|
111
|
+
sessionName: ctx.sessionManager?.getSessionName?.(),
|
|
112
|
+
ombSessionName: process.env.BIZAR_OMP_SESSION_NAME,
|
|
113
|
+
cwd: ctx.cwd ?? process.cwd(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
100
116
|
function bindingMatches(binding, ctx) {
|
|
101
117
|
const cwd = ctx.cwd ?? process.cwd();
|
|
102
118
|
const owner = nativeSessionReference(ctx) ?? `session:${cwd}`;
|
|
@@ -416,34 +432,39 @@ export default function bizarExtension(pi) {
|
|
|
416
432
|
let binding;
|
|
417
433
|
// Main orchestrator role is opt-in: task children must NEVER see it.
|
|
418
434
|
let orchestratorActive = false;
|
|
435
|
+
// The tab title is owned here because an extension override is rendered
|
|
436
|
+
// verbatim: OMP's own run-state spinner would never animate it. A Bizar host
|
|
437
|
+
// would otherwise show the bare binary name in every tab, with nothing to
|
|
438
|
+
// tell a working session from an idle one.
|
|
439
|
+
const sessionTitle = createSessionTitleController({
|
|
440
|
+
write: title => extensionUi?.setTitle?.(title),
|
|
441
|
+
label: process.env.BIZAR_OMP_SESSION_NAME,
|
|
442
|
+
});
|
|
443
|
+
let extensionUi;
|
|
419
444
|
let currentNativeFile;
|
|
420
445
|
let nativeLease;
|
|
421
|
-
let pendingNativeFile;
|
|
422
|
-
let pendingNativeLease;
|
|
423
446
|
const omniRouteResponses = new Map();
|
|
424
447
|
const releaseNativeLease = () => {
|
|
425
|
-
try {
|
|
426
|
-
pendingNativeLease?.release();
|
|
427
|
-
}
|
|
428
|
-
catch { /* teardown remains best-effort */ }
|
|
429
448
|
try {
|
|
430
449
|
nativeLease?.release();
|
|
431
450
|
}
|
|
432
451
|
catch { /* teardown remains best-effort */ }
|
|
433
|
-
pendingNativeLease = undefined;
|
|
434
452
|
nativeLease = undefined;
|
|
435
|
-
pendingNativeFile = undefined;
|
|
436
453
|
currentNativeFile = undefined;
|
|
437
454
|
};
|
|
455
|
+
/** Records which journal this host writes. Returns false when it could not. */
|
|
438
456
|
const registerNativeFile = async (ctx, file) => {
|
|
439
457
|
if (!file || !existsSync(file))
|
|
440
|
-
return;
|
|
458
|
+
return false;
|
|
441
459
|
const ombName = process.env.BIZAR_OMP_SESSION_NAME;
|
|
442
|
-
if (ombName
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
if (!ombName || !isOmbSessionName(ombName))
|
|
461
|
+
return true;
|
|
462
|
+
const indexed = await indexOmbSessionFile(ombName, file, ctx.cwd ?? process.cwd());
|
|
463
|
+
if (!indexed.ok) {
|
|
464
|
+
ctx.ui?.notify(`Bizar could not expose this conversation in OMP /resume: ${indexed.reason}. The private journal remains available through OMB.`, "warning");
|
|
465
|
+
return false;
|
|
446
466
|
}
|
|
467
|
+
return true;
|
|
447
468
|
};
|
|
448
469
|
const currentBinding = (ctx) => {
|
|
449
470
|
if (!binding || !bindingMatches(binding, ctx))
|
|
@@ -468,6 +489,12 @@ export default function bizarExtension(pi) {
|
|
|
468
489
|
binding = undefined;
|
|
469
490
|
orchestratorActive = false;
|
|
470
491
|
releaseNativeLease();
|
|
492
|
+
extensionUi = ctx.ui;
|
|
493
|
+
// The tab should name the conversation. OMP's own session name is the bare
|
|
494
|
+
// binary and the Bizar host name is a hashed directory id, so neither is
|
|
495
|
+
// readable; OMP's session name is its generated title once one exists, and
|
|
496
|
+
// the project directory is the sensible fallback before that.
|
|
497
|
+
sessionTitle.setLabel(sessionTitleLabel(ctx));
|
|
471
498
|
const file = ctx.sessionManager?.getSessionFile?.();
|
|
472
499
|
if (file && existsSync(file)) {
|
|
473
500
|
try {
|
|
@@ -494,15 +521,18 @@ export default function bizarExtension(pi) {
|
|
|
494
521
|
return;
|
|
495
522
|
if (currentNativeFile && sameOmpSessionFile(currentNativeFile, target))
|
|
496
523
|
return;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
524
|
+
// Point-in-time conflict probe, not ownership. OMP 18.4.2 emits
|
|
525
|
+
// `session_switch` only when the switch commits, and offers no cancel or
|
|
526
|
+
// failure event for it: a rejected cwd change or a failed load returns or
|
|
527
|
+
// throws with nothing to observe. A lease retained across that gap would
|
|
528
|
+
// therefore lock the target journal until shutdown, and a timer is not an
|
|
529
|
+
// option — a slow but valid load could outlive it and reopen the very
|
|
530
|
+
// two-writer race the lease exists to prevent. So the probe is taken and
|
|
531
|
+
// dropped immediately, and exclusivity is re-established for real in
|
|
532
|
+
// `session_switch`. This proves the target was free a moment ago; it does
|
|
533
|
+
// not hold it for the duration of the switch.
|
|
502
534
|
try {
|
|
503
|
-
|
|
504
|
-
pendingNativeLease = acquireOmpSessionLease(target);
|
|
505
|
-
pendingNativeFile = target;
|
|
535
|
+
acquireOmpSessionLease(target).release();
|
|
506
536
|
}
|
|
507
537
|
catch (error) {
|
|
508
538
|
ctx.ui?.notify(`Cannot resume this conversation while another OMP/OMB process owns it: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
@@ -518,47 +548,146 @@ export default function bizarExtension(pi) {
|
|
|
518
548
|
catch { /* best-effort */ }
|
|
519
549
|
nativeLease = undefined;
|
|
520
550
|
currentNativeFile = undefined;
|
|
521
|
-
pendingNativeLease = undefined;
|
|
522
|
-
pendingNativeFile = undefined;
|
|
523
551
|
return;
|
|
524
552
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
553
|
+
// Exclusivity is re-established here rather than held from the preflight:
|
|
554
|
+
// OMP offers no switch-cancel event, so a lease kept across that gap would
|
|
555
|
+
// lock the target until shutdown. The new journal is claimed *before* the
|
|
556
|
+
// old one is let go, so this host is never briefly unowned, and a race
|
|
557
|
+
// that another process won between the two events fails here instead of
|
|
558
|
+
// leaving two writers on one journal.
|
|
559
|
+
let claimed;
|
|
560
|
+
try {
|
|
561
|
+
claimed = acquireOmpSessionLease(file);
|
|
533
562
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
563
|
+
catch (error) {
|
|
564
|
+
currentNativeFile = file;
|
|
565
|
+
ctx.ui?.notify(`This OMP session could not claim its conversation journal: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
566
|
+
ctx.shutdown?.();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
nativeLease?.release();
|
|
571
|
+
}
|
|
572
|
+
catch { /* best-effort */ }
|
|
573
|
+
nativeLease = claimed;
|
|
574
|
+
const previousFile = currentNativeFile;
|
|
575
|
+
currentNativeFile = file;
|
|
576
|
+
// A host that switched conversations must move two records together: the
|
|
577
|
+
// tmux generation binding (what a dead-pane respawn is checked against) and
|
|
578
|
+
// the journal manifest (what the hub reads). OMP has already switched by
|
|
579
|
+
// this point, so leaving both on the old journal would make the respawn
|
|
580
|
+
// check *pass* against a conversation this host no longer writes — a silent
|
|
581
|
+
// divergence. Every failure below therefore ends by clearing the binding,
|
|
582
|
+
// which makes that check refuse. Restoring the old generation is never the
|
|
583
|
+
// safe move here.
|
|
584
|
+
const ombName = process.env.BIZAR_OMP_SESSION_NAME;
|
|
585
|
+
const hosted = Boolean(ombName && isOmbSessionName(ombName) && process.env.TMUX);
|
|
586
|
+
const switched = Boolean(previousFile && !sameOmpSessionFile(previousFile, file));
|
|
587
|
+
const failClosed = async (reason) => {
|
|
588
|
+
// Two independent authorities gate a respawn: the tmux generation binding
|
|
589
|
+
// and the owner link manifest. Invalidate at least one, so a respawn can
|
|
590
|
+
// never pass against the conversation this host has left. A tombstone is
|
|
591
|
+
// deliberately not used here: it hides a live host instead of poisoning
|
|
592
|
+
// its recovery, and this host is still running.
|
|
593
|
+
const cleared = await clearOmbHostGeneration(ombName, ctx.cwd ?? process.cwd());
|
|
594
|
+
const delinked = clearOmbSessionReference(ombName);
|
|
595
|
+
if (cleared || delinked) {
|
|
596
|
+
ctx.ui?.notify(`This host resumed the conversation, but ${reason} Its recovery was invalidated, so a dead pane will refuse to respawn rather than resume a different conversation. Restart this host to continue working.`, "warning");
|
|
546
597
|
return;
|
|
547
598
|
}
|
|
599
|
+
// Neither authority could be invalidated, so the old generation and the
|
|
600
|
+
// old manifest still agree and a respawn would pass against the
|
|
601
|
+
// conversation this host has left. There is no honest way to describe
|
|
602
|
+
// that as fail-closed, so the host is stopped rather than left running on
|
|
603
|
+
// records that no longer describe it.
|
|
604
|
+
ctx.ui?.notify(`This host resumed the conversation, but ${reason} Its recovery records could not be invalidated, so it still describes the previous conversation. Stopping this host; close and reopen it before continuing.`, "error");
|
|
605
|
+
ctx.shutdown?.();
|
|
606
|
+
};
|
|
607
|
+
if (switched && hosted) {
|
|
608
|
+
const generation = await readOmbJournalNativeId(file);
|
|
609
|
+
if (!generation)
|
|
610
|
+
await failClosed("the resumed journal has no readable session header.");
|
|
611
|
+
else {
|
|
612
|
+
const rebound = await rebindOmbHostGeneration(ombName, generation, ctx.cwd ?? process.cwd());
|
|
613
|
+
if (!rebound.ok)
|
|
614
|
+
await failClosed(`its generation binding could not follow (${rebound.reason}).`);
|
|
615
|
+
else if (await registerNativeFile(ctx, file)) {
|
|
616
|
+
ctx.ui?.notify(`This host now writes the conversation you selected; its generation binding moved from ${rebound.previous ?? "unset"} to ${generation}.`, "info");
|
|
617
|
+
}
|
|
618
|
+
else
|
|
619
|
+
await failClosed("its journal manifest could not be updated.");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
await registerNativeFile(ctx, file);
|
|
548
624
|
}
|
|
549
|
-
currentNativeFile = file;
|
|
550
625
|
});
|
|
551
|
-
pi.on("session_shutdown", async () => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
626
|
+
pi.on("session_shutdown", async () => {
|
|
627
|
+
releaseNativeLease();
|
|
628
|
+
binding = undefined;
|
|
629
|
+
orchestratorActive = false;
|
|
630
|
+
sessionTitle.dispose();
|
|
631
|
+
extensionUi = undefined;
|
|
632
|
+
});
|
|
633
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
634
|
+
extensionUi = ctx?.ui ?? extensionUi;
|
|
635
|
+
sessionTitle.setState("working");
|
|
636
|
+
return {
|
|
637
|
+
systemPrompt: appendUserIdentityInvariant(orchestratorActive
|
|
638
|
+
? applyOrchestratorPolicy(appendBizarInvariant(paramsOf(event).systemPrompt))
|
|
639
|
+
: appendBizarInvariant(paramsOf(event).systemPrompt)),
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
pi.on("agent_end", (event, ctx) => {
|
|
643
|
+
extensionUi = ctx?.ui ?? extensionUi;
|
|
644
|
+
// A scheduled continuation is not a user-visible settle, so the spinner
|
|
645
|
+
// keeps running across an automatic retry instead of flickering idle.
|
|
646
|
+
if (paramsOf(event).willContinue === true)
|
|
647
|
+
return;
|
|
648
|
+
// Re-read the label: OMP generates the conversation title during the turn,
|
|
649
|
+
// so the tab should pick it up as soon as it exists.
|
|
650
|
+
if (ctx)
|
|
651
|
+
sessionTitle.setLabel(sessionTitleLabel(ctx));
|
|
652
|
+
sessionTitle.setState("idle");
|
|
653
|
+
});
|
|
557
654
|
// A002: managed task isolation/admission pre-execution gate.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
655
|
+
//
|
|
656
|
+
// `AGENT_ROLES` is the canonical list of packaged specialists and is written
|
|
657
|
+
// with the hyphens the `agents/*.md` front matter actually uses. The gate
|
|
658
|
+
// previously carried its own underscored copy, so every valid call to a real
|
|
659
|
+
// packaged specialist was refused as a non-packaged agent.
|
|
660
|
+
const BIZAR_PACKAGED_AGENTS = Object.fromEntries(AGENT_ROLES.map(name => [name, true]));
|
|
661
|
+
/**
|
|
662
|
+
* Every task in one call, in the shape the model sent it.
|
|
663
|
+
*
|
|
664
|
+
* Bizar configures OMP with `task.batch=true`, so a valid call carries
|
|
665
|
+
* `tasks: [{ agent, isolated, ... }]` with no top-level `agent`/`isolated`.
|
|
666
|
+
* Reading only the flat shape rejected every batch call before OMP could run
|
|
667
|
+
* it, and treated a batch of individually-valid tasks as unadmitted because
|
|
668
|
+
* the top-level fields were absent.
|
|
669
|
+
*/
|
|
670
|
+
const requestedTasks = (input) => {
|
|
671
|
+
if (!Array.isArray(input.tasks)) {
|
|
672
|
+
return [{ agent: typeof input.agent === "string" ? input.agent : "", isolated: input.isolated === true }];
|
|
673
|
+
}
|
|
674
|
+
return input.tasks.map(entry => {
|
|
675
|
+
const task = paramsOf(entry);
|
|
676
|
+
return { agent: typeof task.agent === "string" ? task.agent : "", isolated: task.isolated === true };
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
/**
|
|
680
|
+
* Name the run that closed the gate and how to reopen it.
|
|
681
|
+
*
|
|
682
|
+
* A run left mid-flight — a sleep, a kill, a Ctrl-C — keeps the managed gates
|
|
683
|
+
* engaged for the rest of the session, and the agent has no way to notice
|
|
684
|
+
* that from the outside. The refusal is the one message it reliably reads at
|
|
685
|
+
* the moment it is refused, so it carries the run id, its phase, and the
|
|
686
|
+
* cancel that releases it.
|
|
687
|
+
*/
|
|
688
|
+
const managedRunNotice = (run) => ` A managed Bizar run (${run?.runId ?? "unknown"}) is active in phase "${run?.phase ?? "unknown"}"; `
|
|
689
|
+
+ "this stays blocked until that run finishes. To release it now, call `bizar_workflow` with action \"cancel\" and runId \""
|
|
690
|
+
+ `${run?.runId ?? "unknown"}\".`;
|
|
562
691
|
pi.on("tool_call", (event, ctx) => {
|
|
563
692
|
try {
|
|
564
693
|
const params = paramsOf(event);
|
|
@@ -567,16 +696,15 @@ export default function bizarExtension(pi) {
|
|
|
567
696
|
return;
|
|
568
697
|
const active = currentBinding(ctx);
|
|
569
698
|
const run = active.journal.snapshot().run;
|
|
570
|
-
|
|
571
|
-
if (!managedActive)
|
|
699
|
+
if (!isRunPhaseActive(run?.phase))
|
|
572
700
|
return;
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
701
|
+
const tasks = requestedTasks(paramsOf(params.input));
|
|
702
|
+
for (const task of tasks) {
|
|
703
|
+
if (!task.isolated)
|
|
704
|
+
return { block: true, reason: `A002: managed workflow is active; native \`task\` calls must declare \`isolated: true\` on every task.${managedRunNotice(run)}` };
|
|
705
|
+
if (!BIZAR_PACKAGED_AGENTS[task.agent])
|
|
706
|
+
return { block: true, reason: `A002: managed workflow refused native \`task\` to non-packaged agent ${task.agent || "(unspecified)"}; only packaged Bizar specialists are admitted.${managedRunNotice(run)}` };
|
|
707
|
+
}
|
|
580
708
|
}
|
|
581
709
|
catch {
|
|
582
710
|
return;
|
|
@@ -592,14 +720,13 @@ export default function bizarExtension(pi) {
|
|
|
592
720
|
return;
|
|
593
721
|
const active = currentBinding(ctx);
|
|
594
722
|
const run = active.journal.snapshot().run;
|
|
595
|
-
|
|
596
|
-
if (!managedActive)
|
|
723
|
+
if (!isRunPhaseActive(run?.phase))
|
|
597
724
|
return;
|
|
598
725
|
const activeDispatch = active.journal.snapshot().dispatches.find(intent => intent.runId === run.runId && !["settled", "failed", "cancelled", "unknown-outcome", "rejected"].includes(intent.status));
|
|
599
726
|
if (!activeDispatch?.tools || activeDispatch.tools.length === 0)
|
|
600
727
|
return;
|
|
601
728
|
if (!activeDispatch.tools.includes(toolName))
|
|
602
|
-
return { block: true, reason: `A004: managed dispatch ${activeDispatch.id} declared tools allowlist; tool "${toolName}" is not permitted (allowed: ${activeDispatch.tools.join(", ")})
|
|
729
|
+
return { block: true, reason: `A004: managed dispatch ${activeDispatch.id} declared tools allowlist; tool "${toolName}" is not permitted (allowed: ${activeDispatch.tools.join(", ")}).${managedRunNotice(run)}` };
|
|
603
730
|
}
|
|
604
731
|
catch {
|
|
605
732
|
return;
|
|
@@ -614,16 +741,25 @@ export default function bizarExtension(pi) {
|
|
|
614
741
|
return result({ orchestratorActive: true, scope: "main-session-only", note: "Main orchestrator policy will be appended to subsequent agent system prompts in this session. Task children must not invoke this tool." });
|
|
615
742
|
},
|
|
616
743
|
});
|
|
617
|
-
// A008: operator receipt
|
|
744
|
+
// A008: operator receipt — required before an operator-judgment criterion is
|
|
745
|
+
// covered.
|
|
746
|
+
//
|
|
747
|
+
// The operator identity is taken from the environment, not from the call
|
|
748
|
+
// arguments. A model-callable tool whose caller supplies its own `operatorId`
|
|
749
|
+
// cannot attest that a human gave a verdict; that made the receipt a content
|
|
750
|
+
// checksum masquerading as operator authorization. The receipt now records
|
|
751
|
+
// *who the host attributes the verdict to*, and refuses to issue one at all
|
|
752
|
+
// when no operator is configured — so a run that demands operator judgment
|
|
753
|
+
// fails closed instead of being self-approved.
|
|
618
754
|
pi.registerTool({
|
|
619
755
|
name: "bizar_operator_receipt", label: "Bizar Operator Receipt",
|
|
620
|
-
description: "Record
|
|
756
|
+
description: "Record the configured operator's verdict on an operator-judgment criterion. The operator identity comes from the environment, not from this call.",
|
|
621
757
|
parameters: pi.zod.object({
|
|
622
758
|
workItemId: pi.zod.string(),
|
|
623
|
-
|
|
759
|
+
criterionId: pi.zod.string(),
|
|
760
|
+
evidenceId: pi.zod.string().optional(),
|
|
624
761
|
verdict: pi.zod.enum(["passed", "failed", "blocked"]),
|
|
625
762
|
rationale: pi.zod.string(),
|
|
626
|
-
operatorId: pi.zod.string(),
|
|
627
763
|
}),
|
|
628
764
|
async execute(_id, raw, _signal, _onUpdate, ctx) {
|
|
629
765
|
try {
|
|
@@ -632,20 +768,25 @@ export default function bizarExtension(pi) {
|
|
|
632
768
|
const state = active.journal.snapshot();
|
|
633
769
|
if (!state.run)
|
|
634
770
|
throw new Error("No active workflow run");
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
771
|
+
// Never taken from the call: a caller must not be able to name itself.
|
|
772
|
+
const operatorId = process.env.BIZAR_OPERATOR_ID?.trim() || "";
|
|
773
|
+
if (!operatorId)
|
|
774
|
+
throw new Error("No operator is configured; set BIZAR_OPERATOR_ID to the identity that may record operator verdicts");
|
|
775
|
+
const evidenceId = typeof p.evidenceId === "string" ? p.evidenceId : "";
|
|
776
|
+
if (evidenceId && !state.evidence.some(record => record.id === evidenceId))
|
|
777
|
+
throw new Error(`Evidence ${evidenceId} not found`);
|
|
638
778
|
const receiptId = `receipt_${randomUUID()}`;
|
|
639
|
-
const
|
|
640
|
-
const signedContentHash = createHash("sha256").update(signedContent).digest("hex");
|
|
779
|
+
const issuedAt = new Date().toISOString();
|
|
641
780
|
const verdict = String(p.verdict);
|
|
781
|
+
const signedContent = JSON.stringify({ workItemId: p.workItemId, evidenceId, verdict, operatorId, issuedAt });
|
|
782
|
+
const signedContentHash = createHash("sha256").update(signedContent).digest("hex");
|
|
642
783
|
const receipt = {
|
|
643
784
|
id: receiptId, runId: state.run.runId, workItemId: String(p.workItemId),
|
|
644
|
-
evidenceId
|
|
645
|
-
operatorId
|
|
785
|
+
evidenceId, verdict, rationale: String(p.rationale),
|
|
786
|
+
operatorId, signedContentHash, issuedAt,
|
|
646
787
|
};
|
|
647
788
|
active.journal.append({ type: "operator.receipt.recorded", runId: state.run.runId, receipt }, `receipt:${receiptId}`, state.revision);
|
|
648
|
-
return result({ receiptId, verdict
|
|
789
|
+
return result({ receiptId, verdict, operatorId, signedContentHash });
|
|
649
790
|
}
|
|
650
791
|
catch (error) {
|
|
651
792
|
return failure(error);
|