open-multi-agent-kit 0.78.0 → 0.78.1
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 +44 -15
- package/MATURITY.md +2 -2
- package/README.md +56 -26
- package/ROADMAP.md +36 -28
- package/dist/cli/register-basic-commands.js +3 -2
- package/dist/cli/register-mcp-dag-cron-screenshot-commands.js +2 -0
- package/dist/cli/register-tool-commands.js +11 -0
- package/dist/cli/register-workflow-commands.js +1 -0
- package/dist/cli/registry/tooling.js +3 -2
- package/dist/commands/chat/core.js +5 -0
- package/dist/commands/chat/native-root-loop.js +60 -0
- package/dist/commands/dag-from-spec.d.ts +1 -0
- package/dist/commands/dag-from-spec.js +61 -1
- package/dist/commands/graph.d.ts +62 -0
- package/dist/commands/graph.js +182 -0
- package/dist/commands/merge.d.ts +1 -0
- package/dist/commands/merge.js +88 -0
- package/dist/commands/parallel/core.js +3 -3
- package/dist/commands/provider.js +5 -3
- package/dist/commands/star.js +6 -1
- package/dist/commands/summary.d.ts +4 -1
- package/dist/commands/summary.js +103 -1
- package/dist/commands/team.d.ts +1 -0
- package/dist/commands/team.js +38 -0
- package/dist/contracts/provider-health.d.ts +42 -0
- package/dist/contracts/provider-health.js +9 -0
- package/dist/goal/intent-frame.d.ts +24 -0
- package/dist/goal/intent-frame.js +18 -0
- package/dist/memory/local-graph-memory-store.d.ts +15 -0
- package/dist/memory/local-graph-memory-store.js +176 -0
- package/dist/memory/memory-store.d.ts +18 -0
- package/dist/memory/memory-store.js +18 -0
- package/dist/orchestration/adaptorch-topology.d.ts +59 -0
- package/dist/orchestration/adaptorch-topology.js +194 -0
- package/dist/orchestration/capability-routing.d.ts +23 -0
- package/dist/orchestration/capability-routing.js +56 -0
- package/dist/orchestration/dag-compiler-types.d.ts +3 -0
- package/dist/orchestration/dag-compiler.js +14 -1
- package/dist/orchestration/parallel-orchestrator.d.ts +6 -0
- package/dist/orchestration/parallel-orchestrator.js +31 -0
- package/dist/providers/provider-health.d.ts +39 -0
- package/dist/providers/provider-health.js +161 -0
- package/dist/runtime/context-broker.d.ts +13 -4
- package/dist/runtime/context-broker.js +14 -1
- package/dist/runtime/headroom-policy.d.ts +37 -0
- package/dist/runtime/headroom-policy.js +122 -0
- package/dist/runtime/ouroboros-policy.d.ts +57 -0
- package/dist/runtime/ouroboros-policy.js +134 -0
- package/dist/runtime/runtime-backed-task-runner.js +9 -1
- package/dist/runtime/tool-dispatch-contracts.d.ts +57 -1
- package/dist/runtime/tool-dispatch-contracts.js +79 -3
- package/dist/safety/tool-authority-gate.d.ts +62 -0
- package/dist/safety/tool-authority-gate.js +108 -0
- package/dist/schema/provider.schema.d.ts +4 -4
- package/dist/util/first-run-star.d.ts +9 -0
- package/dist/util/first-run-star.js +42 -1
- package/dist/util/terminal-input.d.ts +20 -0
- package/dist/util/terminal-input.js +32 -0
- package/dist/util/update-check.d.ts +6 -1
- package/dist/util/update-check.js +35 -1
- package/docs/2026-06-08/critical-issues.md +20 -0
- package/docs/2026-06-08/improvements.md +14 -0
- package/docs/2026-06-08/init-checklist.md +25 -0
- package/docs/2026-06-08/plan.md +20 -0
- package/docs/getting-started.md +31 -3
- package/docs/integrations/ouroboros.md +96 -0
- package/docs/provider-maturity.md +1 -1
- package/docs/versioning.md +3 -3
- package/package.json +1 -1
- package/dist/native/linux-x64/omk-safety +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeadroomPolicy — proactive context compaction trigger.
|
|
3
|
+
*
|
|
4
|
+
* Industrial control-loop policy: given current token usage and model
|
|
5
|
+
* context window, decide whether to compact BEFORE utilization reaches
|
|
6
|
+
* a configurable threshold (default 90%).
|
|
7
|
+
*
|
|
8
|
+
* Prefers the external `headroom` CLI compressor when available;
|
|
9
|
+
* falls back to the built-in `optimizeContextBudget` when not.
|
|
10
|
+
*
|
|
11
|
+
* Deterministic except for the injectable `runHeadroom` runner.
|
|
12
|
+
*/
|
|
13
|
+
export interface HeadroomDecision {
|
|
14
|
+
readonly shouldCompact: boolean;
|
|
15
|
+
readonly utilization: number;
|
|
16
|
+
readonly threshold: number;
|
|
17
|
+
readonly usedTokens: number;
|
|
18
|
+
readonly contextWindow: number;
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface HeadroomCompactResult {
|
|
22
|
+
readonly compacted: boolean;
|
|
23
|
+
readonly via: "headroom" | "fallback" | "none";
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveHeadroomThreshold(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): number;
|
|
26
|
+
export declare function isHeadroomEnabled(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): boolean;
|
|
27
|
+
export declare function evaluateHeadroom(input: {
|
|
28
|
+
usedTokens: number;
|
|
29
|
+
contextWindow: number;
|
|
30
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
31
|
+
}): HeadroomDecision;
|
|
32
|
+
export declare function maybeCompactWithHeadroom(args: {
|
|
33
|
+
decision: HeadroomDecision;
|
|
34
|
+
text?: string;
|
|
35
|
+
runHeadroom?: (text: string) => Promise<string | null>;
|
|
36
|
+
fallback?: () => Promise<void>;
|
|
37
|
+
}): Promise<HeadroomCompactResult>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeadroomPolicy — proactive context compaction trigger.
|
|
3
|
+
*
|
|
4
|
+
* Industrial control-loop policy: given current token usage and model
|
|
5
|
+
* context window, decide whether to compact BEFORE utilization reaches
|
|
6
|
+
* a configurable threshold (default 90%).
|
|
7
|
+
*
|
|
8
|
+
* Prefers the external `headroom` CLI compressor when available;
|
|
9
|
+
* falls back to the built-in `optimizeContextBudget` when not.
|
|
10
|
+
*
|
|
11
|
+
* Deterministic except for the injectable `runHeadroom` runner.
|
|
12
|
+
*/
|
|
13
|
+
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
14
|
+
const DEFAULT_THRESHOLD = 0.90;
|
|
15
|
+
const MIN_THRESHOLD = 0.50;
|
|
16
|
+
const MAX_THRESHOLD = 0.99;
|
|
17
|
+
// ─── Threshold resolver ──────────────────────────────────────────────────────
|
|
18
|
+
export function resolveHeadroomThreshold(env = process.env) {
|
|
19
|
+
const raw = env.OMK_HEADROOM_THRESHOLD;
|
|
20
|
+
if (raw === undefined || raw === "")
|
|
21
|
+
return DEFAULT_THRESHOLD;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
if (!Number.isFinite(parsed))
|
|
24
|
+
return DEFAULT_THRESHOLD;
|
|
25
|
+
return Math.min(MAX_THRESHOLD, Math.max(MIN_THRESHOLD, parsed));
|
|
26
|
+
}
|
|
27
|
+
// ─── Enabled check ───────────────────────────────────────────────────────────
|
|
28
|
+
export function isHeadroomEnabled(env = process.env) {
|
|
29
|
+
const raw = env.OMK_HEADROOM;
|
|
30
|
+
if (raw === undefined || raw === "")
|
|
31
|
+
return true;
|
|
32
|
+
const normalized = raw.trim().toLowerCase();
|
|
33
|
+
if (normalized === "off" || normalized === "0" || normalized === "false")
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// ─── Evaluate ────────────────────────────────────────────────────────────────
|
|
38
|
+
export function evaluateHeadroom(input) {
|
|
39
|
+
const env = input.env ?? process.env;
|
|
40
|
+
const enabled = isHeadroomEnabled(env);
|
|
41
|
+
const threshold = resolveHeadroomThreshold(env);
|
|
42
|
+
const { usedTokens, contextWindow } = input;
|
|
43
|
+
if (!enabled) {
|
|
44
|
+
return {
|
|
45
|
+
shouldCompact: false,
|
|
46
|
+
utilization: 0,
|
|
47
|
+
threshold,
|
|
48
|
+
usedTokens,
|
|
49
|
+
contextWindow,
|
|
50
|
+
reason: "headroom disabled via OMK_HEADROOM",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (contextWindow <= 0) {
|
|
54
|
+
return {
|
|
55
|
+
shouldCompact: false,
|
|
56
|
+
utilization: 0,
|
|
57
|
+
threshold,
|
|
58
|
+
usedTokens,
|
|
59
|
+
contextWindow,
|
|
60
|
+
reason: "context window size unknown (0)",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const utilization = usedTokens / contextWindow;
|
|
64
|
+
const shouldCompact = utilization >= threshold;
|
|
65
|
+
return {
|
|
66
|
+
shouldCompact,
|
|
67
|
+
utilization,
|
|
68
|
+
threshold,
|
|
69
|
+
usedTokens,
|
|
70
|
+
contextWindow,
|
|
71
|
+
reason: shouldCompact
|
|
72
|
+
? `utilization ${(utilization * 100).toFixed(1)}% >= threshold ${(threshold * 100).toFixed(1)}%`
|
|
73
|
+
: `utilization ${(utilization * 100).toFixed(1)}% below threshold ${(threshold * 100).toFixed(1)}%`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ─── Compaction runner ───────────────────────────────────────────────────────
|
|
77
|
+
const HEADROOM_CLI_TIMEOUT_MS = 15_000;
|
|
78
|
+
async function defaultRunHeadroom(text) {
|
|
79
|
+
try {
|
|
80
|
+
const { runShell } = await import("../util/shell.js");
|
|
81
|
+
const result = await runShell("headroom", ["compact", "--stdin"], {
|
|
82
|
+
timeout: HEADROOM_CLI_TIMEOUT_MS,
|
|
83
|
+
input: text,
|
|
84
|
+
});
|
|
85
|
+
if (result.failed || result.exitCode !== 0)
|
|
86
|
+
return null;
|
|
87
|
+
const output = result.stdout.trim();
|
|
88
|
+
return output.length > 0 ? output : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function maybeCompactWithHeadroom(args) {
|
|
95
|
+
if (!args.decision.shouldCompact) {
|
|
96
|
+
return { compacted: false, via: "none" };
|
|
97
|
+
}
|
|
98
|
+
// Try headroom first
|
|
99
|
+
if (args.text) {
|
|
100
|
+
try {
|
|
101
|
+
const runner = args.runHeadroom ?? defaultRunHeadroom;
|
|
102
|
+
const result = await runner(args.text);
|
|
103
|
+
if (result !== null) {
|
|
104
|
+
return { compacted: true, via: "headroom" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Headroom threw — fall through to fallback
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Fall back to built-in optimizer
|
|
112
|
+
if (args.fallback) {
|
|
113
|
+
try {
|
|
114
|
+
await args.fallback();
|
|
115
|
+
return { compacted: true, via: "fallback" };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Fallback also failed — graceful degradation
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { compacted: false, via: "none" };
|
|
122
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ouroboros routing policy for OMK.
|
|
3
|
+
*
|
|
4
|
+
* Resolves whether to prefer the embedded Ouroboros spec-first flow
|
|
5
|
+
* for goal/spec/orchestration intents. Detection is non-fatal and
|
|
6
|
+
* never triggers network access or implicit installs.
|
|
7
|
+
*/
|
|
8
|
+
export type OuroborosMode = "always" | "auto" | "off";
|
|
9
|
+
/**
|
|
10
|
+
* Read the OMK_OUROBOROS env var and normalise it into a mode.
|
|
11
|
+
*
|
|
12
|
+
* - unset / anything other than the known tokens → "always" (default)
|
|
13
|
+
* - "auto" → "auto"
|
|
14
|
+
* - "off" / "0" / "false" (case-insensitive) → "off"
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveOuroborosMode(env?: NodeJS.ProcessEnv): OuroborosMode;
|
|
17
|
+
export interface OuroborosAvailability {
|
|
18
|
+
available: boolean;
|
|
19
|
+
via: "mcp" | "binary" | "none";
|
|
20
|
+
detail: string;
|
|
21
|
+
}
|
|
22
|
+
interface DetectOpts {
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
mcpConfigPath?: string;
|
|
25
|
+
which?: (cmd: string) => Promise<string | null>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check whether Ouroboros is reachable without network or installs.
|
|
29
|
+
*
|
|
30
|
+
* Strategy (non-fatal):
|
|
31
|
+
* 1. Look for an `ouroboros` key in ~/.omk/agent/mcp.json (global)
|
|
32
|
+
* and/or .omk/mcp.json (project-scoped).
|
|
33
|
+
* 2. Optionally check for an `ouroboros` binary via an injectable which().
|
|
34
|
+
*
|
|
35
|
+
* Any I/O error silently yields `{ available: false, via: "none" }`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function detectOuroborosAvailable(opts?: DetectOpts): Promise<OuroborosAvailability>;
|
|
38
|
+
export interface OuroborosDecision {
|
|
39
|
+
use: boolean;
|
|
40
|
+
mode: OuroborosMode;
|
|
41
|
+
availability: OuroborosAvailability;
|
|
42
|
+
reason: string;
|
|
43
|
+
}
|
|
44
|
+
interface DecisionInput {
|
|
45
|
+
intent: string;
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
detect?: () => Promise<OuroborosAvailability>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Decide whether the current run should route through Ouroboros.
|
|
51
|
+
*
|
|
52
|
+
* - use=true only when mode≠off AND available AND intent matches.
|
|
53
|
+
* - When mode=always but unavailable → use=false with fallback reason.
|
|
54
|
+
* - Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveOuroborosDecision(input: DecisionInput): Promise<OuroborosDecision>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ouroboros routing policy for OMK.
|
|
3
|
+
*
|
|
4
|
+
* Resolves whether to prefer the embedded Ouroboros spec-first flow
|
|
5
|
+
* for goal/spec/orchestration intents. Detection is non-fatal and
|
|
6
|
+
* never triggers network access or implicit installs.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
/**
|
|
12
|
+
* Read the OMK_OUROBOROS env var and normalise it into a mode.
|
|
13
|
+
*
|
|
14
|
+
* - unset / anything other than the known tokens → "always" (default)
|
|
15
|
+
* - "auto" → "auto"
|
|
16
|
+
* - "off" / "0" / "false" (case-insensitive) → "off"
|
|
17
|
+
*/
|
|
18
|
+
export function resolveOuroborosMode(env = process.env) {
|
|
19
|
+
const raw = (env.OMK_OUROBOROS ?? "").trim().toLowerCase();
|
|
20
|
+
if (raw === "off" || raw === "0" || raw === "false")
|
|
21
|
+
return "off";
|
|
22
|
+
if (raw === "auto")
|
|
23
|
+
return "auto";
|
|
24
|
+
return "always";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check whether Ouroboros is reachable without network or installs.
|
|
28
|
+
*
|
|
29
|
+
* Strategy (non-fatal):
|
|
30
|
+
* 1. Look for an `ouroboros` key in ~/.omk/agent/mcp.json (global)
|
|
31
|
+
* and/or .omk/mcp.json (project-scoped).
|
|
32
|
+
* 2. Optionally check for an `ouroboros` binary via an injectable which().
|
|
33
|
+
*
|
|
34
|
+
* Any I/O error silently yields `{ available: false, via: "none" }`.
|
|
35
|
+
*/
|
|
36
|
+
export async function detectOuroborosAvailable(opts) {
|
|
37
|
+
// --- binary check (fast, optional) ---
|
|
38
|
+
if (opts?.which) {
|
|
39
|
+
try {
|
|
40
|
+
const bin = await opts.which("ouroboros");
|
|
41
|
+
if (bin) {
|
|
42
|
+
return { available: true, via: "binary", detail: `found binary: ${bin}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// swallow
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// --- MCP config check (non-fatal) ---
|
|
50
|
+
const home = homedir();
|
|
51
|
+
const candidates = [
|
|
52
|
+
opts?.mcpConfigPath ?? join(home, ".omk", "agent", "mcp.json"),
|
|
53
|
+
join(process.cwd(), ".omk", "mcp.json"),
|
|
54
|
+
];
|
|
55
|
+
for (const configPath of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(configPath, "utf-8");
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (isRecord(parsed) && isRecord(parsed.mcpServers)) {
|
|
60
|
+
if ("ouroboros" in parsed.mcpServers) {
|
|
61
|
+
return {
|
|
62
|
+
available: true,
|
|
63
|
+
via: "mcp",
|
|
64
|
+
detail: `ouroboros server found in ${configPath}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// file missing or malformed → continue
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { available: false, via: "none", detail: "ouroboros not detected" };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Intent tokens (lowercased) that qualify for Ouroboros routing.
|
|
77
|
+
*/
|
|
78
|
+
const OUROBOROS_INTENT_KEYWORDS = [
|
|
79
|
+
"goal",
|
|
80
|
+
"plan",
|
|
81
|
+
"spec",
|
|
82
|
+
"seed",
|
|
83
|
+
"interview",
|
|
84
|
+
"orchestrate",
|
|
85
|
+
"feature",
|
|
86
|
+
"build",
|
|
87
|
+
"implement",
|
|
88
|
+
// Korean equivalents
|
|
89
|
+
"계획",
|
|
90
|
+
"스펙",
|
|
91
|
+
"구현",
|
|
92
|
+
"기획",
|
|
93
|
+
];
|
|
94
|
+
/**
|
|
95
|
+
* Decide whether the current run should route through Ouroboros.
|
|
96
|
+
*
|
|
97
|
+
* - use=true only when mode≠off AND available AND intent matches.
|
|
98
|
+
* - When mode=always but unavailable → use=false with fallback reason.
|
|
99
|
+
* - Never throws.
|
|
100
|
+
*/
|
|
101
|
+
export async function resolveOuroborosDecision(input) {
|
|
102
|
+
const mode = resolveOuroborosMode(input.env);
|
|
103
|
+
const detect = input.detect ?? (() => detectOuroborosAvailable({ env: input.env }));
|
|
104
|
+
let availability;
|
|
105
|
+
try {
|
|
106
|
+
availability = await detect();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
availability = { available: false, via: "none", detail: "detection threw" };
|
|
110
|
+
}
|
|
111
|
+
if (mode === "off") {
|
|
112
|
+
return { use: false, mode, availability, reason: "ouroboros-mode-off" };
|
|
113
|
+
}
|
|
114
|
+
if (!availability.available) {
|
|
115
|
+
return {
|
|
116
|
+
use: false,
|
|
117
|
+
mode,
|
|
118
|
+
availability,
|
|
119
|
+
reason: "ouroboros-unavailable-fallback-native",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!isGoalLikeIntent(input.intent)) {
|
|
123
|
+
return { use: false, mode, availability, reason: "intent-not-goal-like" };
|
|
124
|
+
}
|
|
125
|
+
return { use: true, mode, availability, reason: "ouroboros-routing-active" };
|
|
126
|
+
}
|
|
127
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
128
|
+
function isGoalLikeIntent(intent) {
|
|
129
|
+
const lowered = intent.toLocaleLowerCase();
|
|
130
|
+
return OUROBOROS_INTENT_KEYWORDS.some((kw) => lowered.includes(kw));
|
|
131
|
+
}
|
|
132
|
+
function isRecord(v) {
|
|
133
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
134
|
+
}
|
|
@@ -14,6 +14,7 @@ import { applyTaskRunContextToAgentTask, envFromWorkerManifest } from "./worker-
|
|
|
14
14
|
import { createRuntimeRegistry } from "./runtime-registry.js";
|
|
15
15
|
import { createRuntimeRouter } from "./runtime-router.js";
|
|
16
16
|
import { createContextBroker } from "./context-broker.js";
|
|
17
|
+
import { maybeCompactWithHeadroom } from "./headroom-policy.js";
|
|
17
18
|
import { DeepSeekRuntime } from "./deepseek-runtime.js";
|
|
18
19
|
import { CodexRuntime } from "./codex-runtime.js";
|
|
19
20
|
import { createOpencodeCliAdapter } from "../adapters/opencode/opencode-cli-adapter.js";
|
|
@@ -128,7 +129,14 @@ export async function createRuntimeBackedTaskRunner(options) {
|
|
|
128
129
|
startedAt: new Date().toISOString(),
|
|
129
130
|
}
|
|
130
131
|
: undefined;
|
|
131
|
-
const { capsule } = await contextBroker.buildCapsule(node, runState);
|
|
132
|
+
const { capsule, headroomDecision } = await contextBroker.buildCapsule(node, runState);
|
|
133
|
+
// CTX guard: compact via headroom before the context window crosses the threshold (~90%).
|
|
134
|
+
if (headroomDecision?.shouldCompact) {
|
|
135
|
+
await maybeCompactWithHeadroom({
|
|
136
|
+
decision: headroomDecision,
|
|
137
|
+
text: JSON.stringify(capsule),
|
|
138
|
+
}).catch(() => undefined);
|
|
139
|
+
}
|
|
132
140
|
const routing = capsule.node.routing;
|
|
133
141
|
const providerFallbackChain = options.fallbackChain
|
|
134
142
|
?? (routing?.fallbackProvider ? [routing.fallbackProvider] : []);
|
|
@@ -1,8 +1,64 @@
|
|
|
1
1
|
import type { OmkToolCall, OmkToolDefinition } from "./tool-registry-contract.js";
|
|
2
|
+
import { type ToolAuthorityDecision, type ToolOp } from "../safety/tool-authority-gate.js";
|
|
3
|
+
import type { ProviderAuthorityLevel } from "../contracts/provider-health.js";
|
|
2
4
|
export interface ToolDispatchResult<R = unknown> {
|
|
3
5
|
readonly call: OmkToolCall;
|
|
4
6
|
readonly status: "fulfilled" | "rejected";
|
|
5
7
|
readonly value?: R;
|
|
6
8
|
readonly reason?: unknown;
|
|
7
9
|
}
|
|
8
|
-
|
|
10
|
+
/** Shadow = record only; enforce = a block/ask(non-TTY) verdict rejects the call. */
|
|
11
|
+
export type ToolAuthorityMode = "shadow" | "enforce";
|
|
12
|
+
/**
|
|
13
|
+
* Per-call verdict recorded at the dispatch checkpoint. Carries only coarse,
|
|
14
|
+
* non-secret signals (op class, authority levels, policy) — never tool args.
|
|
15
|
+
*/
|
|
16
|
+
export interface ToolAuthorityDecisionRecord {
|
|
17
|
+
readonly toolName: string;
|
|
18
|
+
readonly op: ToolOp;
|
|
19
|
+
readonly decision: ToolAuthorityDecision;
|
|
20
|
+
readonly mode: ToolAuthorityMode;
|
|
21
|
+
/** True only when the verdict actually rejected the call (enforce + block). */
|
|
22
|
+
readonly enforced: boolean;
|
|
23
|
+
/** Redacted, human-readable reason. Never includes args or secret values. */
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Authority wiring for one dispatch turn. All inputs are non-secret enum/bool
|
|
28
|
+
* signals. When omitted from {@link dispatchToolCallsByContract}, dispatch is
|
|
29
|
+
* byte-identical to the ungated path.
|
|
30
|
+
*/
|
|
31
|
+
export interface ToolAuthorityWiring {
|
|
32
|
+
readonly writeAuthority: ProviderAuthorityLevel;
|
|
33
|
+
readonly shellAuthority: ProviderAuthorityLevel;
|
|
34
|
+
readonly approvalPolicy: "interactive" | "auto" | "yolo" | "block";
|
|
35
|
+
readonly sandboxMode: "read-only" | "workspace-write";
|
|
36
|
+
readonly tty: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Enforcement opt-in. Default `false` => shadow mode (zero behavior change):
|
|
39
|
+
* verdicts are computed and recorded but never block. When `true`, a `block`
|
|
40
|
+
* verdict (and `ask` in a non-TTY context, fail-closed) rejects the call.
|
|
41
|
+
*/
|
|
42
|
+
readonly enforce?: boolean;
|
|
43
|
+
/** Optional sink for computed verdicts (invoked in both shadow and enforce). */
|
|
44
|
+
readonly onDecision?: (record: ToolAuthorityDecisionRecord) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the global enforcement opt-in from the environment. Default OFF means
|
|
48
|
+
* the gate runs in shadow mode (record only). Set `OMK_TOOL_AUTHORITY_ENFORCE=1`
|
|
49
|
+
* to enable fail-closed enforcement at the dispatch checkpoint.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveToolAuthorityEnforcement(env?: Record<string, string | undefined>): boolean;
|
|
52
|
+
/** Error used to reject a tool call rejected by the authority gate (enforce mode). */
|
|
53
|
+
export declare class ToolAuthorityBlockedError extends Error {
|
|
54
|
+
readonly toolName: string;
|
|
55
|
+
readonly op: ToolOp;
|
|
56
|
+
readonly decision: ToolAuthorityDecision;
|
|
57
|
+
constructor(record: ToolAuthorityDecisionRecord);
|
|
58
|
+
}
|
|
59
|
+
/** Compute the gate verdict for a single call. Pure (no IO, no env reads). */
|
|
60
|
+
export declare function evaluateToolAuthority(toolName: string, wiring: ToolAuthorityWiring): {
|
|
61
|
+
readonly record: ToolAuthorityDecisionRecord;
|
|
62
|
+
readonly blocked: boolean;
|
|
63
|
+
};
|
|
64
|
+
export declare function dispatchToolCallsByContract<A, R>(calls: readonly OmkToolCall<A>[], registry: ReadonlyMap<string, OmkToolDefinition<A, R>>, dispatchOne: (call: OmkToolCall<A>) => Promise<R>, authority?: ToolAuthorityWiring): Promise<ToolDispatchResult<R>[]>;
|
|
@@ -1,10 +1,86 @@
|
|
|
1
1
|
import { createToolExecutionBatches } from "./tool-registry-contract.js";
|
|
2
|
-
|
|
2
|
+
import { decideToolAuthority, mapToolNameToOp, } from "../safety/tool-authority-gate.js";
|
|
3
|
+
const ENFORCE_PATTERN = /^(1|true|yes|on)$/i;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the global enforcement opt-in from the environment. Default OFF means
|
|
6
|
+
* the gate runs in shadow mode (record only). Set `OMK_TOOL_AUTHORITY_ENFORCE=1`
|
|
7
|
+
* to enable fail-closed enforcement at the dispatch checkpoint.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveToolAuthorityEnforcement(env = process.env) {
|
|
10
|
+
return ENFORCE_PATTERN.test((env.OMK_TOOL_AUTHORITY_ENFORCE ?? "").trim());
|
|
11
|
+
}
|
|
12
|
+
/** Build a redacted reason string from non-secret authority signals only. */
|
|
13
|
+
function redactedAuthorityReason(op, decision, wiring) {
|
|
14
|
+
return (`tool-authority ${decision} for ${op} op ` +
|
|
15
|
+
`(write=${wiring.writeAuthority}, shell=${wiring.shellAuthority}, ` +
|
|
16
|
+
`policy=${wiring.approvalPolicy}, sandbox=${wiring.sandboxMode}, tty=${wiring.tty})`);
|
|
17
|
+
}
|
|
18
|
+
/** Error used to reject a tool call rejected by the authority gate (enforce mode). */
|
|
19
|
+
export class ToolAuthorityBlockedError extends Error {
|
|
20
|
+
toolName;
|
|
21
|
+
op;
|
|
22
|
+
decision;
|
|
23
|
+
constructor(record) {
|
|
24
|
+
super(record.reason);
|
|
25
|
+
this.name = "ToolAuthorityBlockedError";
|
|
26
|
+
this.toolName = record.toolName;
|
|
27
|
+
this.op = record.op;
|
|
28
|
+
this.decision = record.decision;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Compute the gate verdict for a single call. Pure (no IO, no env reads). */
|
|
32
|
+
export function evaluateToolAuthority(toolName, wiring) {
|
|
33
|
+
const op = mapToolNameToOp(toolName);
|
|
34
|
+
const decision = decideToolAuthority({
|
|
35
|
+
op,
|
|
36
|
+
writeAuthority: wiring.writeAuthority,
|
|
37
|
+
shellAuthority: wiring.shellAuthority,
|
|
38
|
+
approvalPolicy: wiring.approvalPolicy,
|
|
39
|
+
sandboxMode: wiring.sandboxMode,
|
|
40
|
+
tty: wiring.tty,
|
|
41
|
+
});
|
|
42
|
+
const enforce = wiring.enforce === true;
|
|
43
|
+
// Fail-closed: a non-TTY "ask" is treated as a block (decideToolAuthority
|
|
44
|
+
// already returns "block" for non-TTY interactive; the second clause is a
|
|
45
|
+
// defensive guard in case the caller ever surfaces "ask" without a TTY).
|
|
46
|
+
const wouldBlock = decision === "block" || (decision === "ask" && !wiring.tty);
|
|
47
|
+
const blocked = enforce && wouldBlock;
|
|
48
|
+
return {
|
|
49
|
+
record: {
|
|
50
|
+
toolName,
|
|
51
|
+
op,
|
|
52
|
+
decision,
|
|
53
|
+
mode: enforce ? "enforce" : "shadow",
|
|
54
|
+
enforced: blocked,
|
|
55
|
+
reason: redactedAuthorityReason(op, decision, wiring),
|
|
56
|
+
},
|
|
57
|
+
blocked,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap a dispatch function with the authority checkpoint. In shadow mode the
|
|
62
|
+
* wrapper records the verdict and always delegates to `dispatchOne`. In enforce
|
|
63
|
+
* mode a blocked verdict rejects the call with a redacted reason.
|
|
64
|
+
*/
|
|
65
|
+
function buildGatedDispatch(wiring, dispatchOne) {
|
|
66
|
+
return async (call) => {
|
|
67
|
+
const { record, blocked } = evaluateToolAuthority(call.toolName, wiring);
|
|
68
|
+
wiring.onDecision?.(record);
|
|
69
|
+
if (blocked) {
|
|
70
|
+
throw new ToolAuthorityBlockedError(record);
|
|
71
|
+
}
|
|
72
|
+
return dispatchOne(call);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function dispatchToolCallsByContract(calls, registry, dispatchOne, authority) {
|
|
76
|
+
// When no authority wiring is supplied the dispatch path is byte-identical to
|
|
77
|
+
// the pre-gate behavior (the checkpoint is a no-op).
|
|
78
|
+
const effectiveDispatch = authority ? buildGatedDispatch(authority, dispatchOne) : dispatchOne;
|
|
3
79
|
const batches = createToolExecutionBatches(calls, registry);
|
|
4
80
|
const appended = [];
|
|
5
81
|
for (const batch of batches) {
|
|
6
82
|
if (batch.kind === "parallel") {
|
|
7
|
-
const settled = await Promise.allSettled(batch.calls.map((call) =>
|
|
83
|
+
const settled = await Promise.allSettled(batch.calls.map((call) => effectiveDispatch(call)));
|
|
8
84
|
settled.forEach((result, index) => {
|
|
9
85
|
const call = batch.calls[index];
|
|
10
86
|
if (!call)
|
|
@@ -15,7 +91,7 @@ export async function dispatchToolCallsByContract(calls, registry, dispatchOne)
|
|
|
15
91
|
}
|
|
16
92
|
for (const call of batch.calls) {
|
|
17
93
|
try {
|
|
18
|
-
appended.push({ call, status: "fulfilled", value: await
|
|
94
|
+
appended.push({ call, status: "fulfilled", value: await effectiveDispatch(call) });
|
|
19
95
|
}
|
|
20
96
|
catch (reason) {
|
|
21
97
|
appended.push({ call, status: "rejected", reason });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tool-authority decision primitive.
|
|
3
|
+
*
|
|
4
|
+
* Decides allow / ask / block for a tool operation given the provider's
|
|
5
|
+
* write/shell authority, the active approval policy, the sandbox mode, and
|
|
6
|
+
* whether a TTY is attached. The function is intentionally pure: no IO, no
|
|
7
|
+
* environment reads, no secrets, no side effects.
|
|
8
|
+
*
|
|
9
|
+
* STATUS (0.78.2 stabilization): this primitive is NOT wired into any live
|
|
10
|
+
* tool-dispatch path. It exists so 0.78.3 can wire it into
|
|
11
|
+
* `dispatchToolCallsByContract` (src/runtime/tool-dispatch-contracts.ts) and
|
|
12
|
+
* the kimi runner tool loop without re-deriving the policy. Zero behavior
|
|
13
|
+
* change to current execution.
|
|
14
|
+
*
|
|
15
|
+
* Design rules (authority outranks policy; fail-closed):
|
|
16
|
+
* 1. read ops are always allowed.
|
|
17
|
+
* 2. a read-only sandbox is a hard floor: any non-read op is blocked.
|
|
18
|
+
* 3. authority must be satisfied for the op before policy is consulted:
|
|
19
|
+
* write -> writeAuthority === "full"
|
|
20
|
+
* shell -> shellAuthority === "full"
|
|
21
|
+
* merge -> writeAuthority === "full" AND shellAuthority === "full"
|
|
22
|
+
* 4. once authority is satisfied, the approval policy decides:
|
|
23
|
+
* block -> block
|
|
24
|
+
* yolo -> allow
|
|
25
|
+
* auto -> allow
|
|
26
|
+
* interactive -> ask when a TTY is present, otherwise block
|
|
27
|
+
* (ask in a non-TTY context = deny-by-default).
|
|
28
|
+
*/
|
|
29
|
+
import type { ProviderAuthorityLevel } from "../contracts/provider-health.js";
|
|
30
|
+
/** Coarse operation class used by the authority gate. */
|
|
31
|
+
export type ToolOp = "read" | "write" | "shell" | "merge";
|
|
32
|
+
/** Gate verdict for a single tool operation. */
|
|
33
|
+
export type ToolAuthorityDecision = "allow" | "ask" | "block";
|
|
34
|
+
/** Inputs to the authority decision. Fully self-contained and side-effect free. */
|
|
35
|
+
export interface ToolAuthorityContext {
|
|
36
|
+
/** Operation class derived from the tool being invoked. */
|
|
37
|
+
readonly op: ToolOp;
|
|
38
|
+
/** Provider authority for write/mutation work. */
|
|
39
|
+
readonly writeAuthority: ProviderAuthorityLevel;
|
|
40
|
+
/** Provider authority for shell/CLI work. */
|
|
41
|
+
readonly shellAuthority: ProviderAuthorityLevel;
|
|
42
|
+
/** Active approval policy for the run. */
|
|
43
|
+
readonly approvalPolicy: "interactive" | "auto" | "yolo" | "block";
|
|
44
|
+
/** Active sandbox mode; "read-only" is a hard floor for non-read ops. */
|
|
45
|
+
readonly sandboxMode: "read-only" | "workspace-write";
|
|
46
|
+
/** Whether an interactive TTY is attached (gates "interactive" -> ask). */
|
|
47
|
+
readonly tty: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Map a raw tool name to its coarse {@link ToolOp} class.
|
|
51
|
+
*
|
|
52
|
+
* Matching is case-insensitive. Unrecognized tools fail closed to the most
|
|
53
|
+
* restrictive sensible op (`shell` = arbitrary effect execution) rather than
|
|
54
|
+
* `read`, so an unknown tool is never silently treated as harmless.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mapToolNameToOp(toolName: string): ToolOp;
|
|
57
|
+
/**
|
|
58
|
+
* Decide allow / ask / block for a tool operation. Pure and fail-closed.
|
|
59
|
+
*
|
|
60
|
+
* @see ToolAuthorityContext for the decision inputs and ordering rules.
|
|
61
|
+
*/
|
|
62
|
+
export declare function decideToolAuthority(ctx: ToolAuthorityContext): ToolAuthorityDecision;
|