@ordewell/core 0.8.0 → 0.9.0
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 +5 -5
- package/dist/{IFileSystem-D96mT-bm.d.ts → IFileSystem-DpaaFdit.d.ts} +1 -1
- package/dist/{IFileSystem-DXJCRw5x.d.mts → IFileSystem-_m7HdoxJ.d.mts} +1 -1
- package/dist/{IRunner-CGCihlGQ.d.ts → IRunner-BiQio8gJ.d.ts} +161 -27
- package/dist/{IRunner-XVUddYVX.d.mts → IRunner-CxiymkP_.d.mts} +161 -27
- package/dist/{chunk-BLLIBCCS.mjs → chunk-QUARDKVT.mjs} +6 -3
- package/dist/chunk-QUARDKVT.mjs.map +1 -0
- package/dist/index.d.mts +33 -9
- package/dist/index.d.ts +33 -9
- package/dist/index.js +725 -112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +712 -109
- package/dist/index.mjs.map +1 -1
- package/dist/{plan-utils-Cv48fe7r.d.ts → plan-utils-6rUr7ZOx.d.ts} +1 -1
- package/dist/{plan-utils-BMayhGBj.d.mts → plan-utils-P3PIEwNa.d.mts} +1 -1
- package/dist/plan-utils.d.mts +2 -2
- package/dist/plan-utils.d.ts +2 -2
- package/dist/plan-utils.js +5 -2
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +1 -1
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +2 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BLLIBCCS.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import {
|
|
|
97
97
|
unsendLatest,
|
|
98
98
|
usageLine,
|
|
99
99
|
validateTaskEdit
|
|
100
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-QUARDKVT.mjs";
|
|
101
101
|
import {
|
|
102
102
|
PLAN_ENVELOPE_KEY,
|
|
103
103
|
PlanParseError,
|
|
@@ -450,7 +450,7 @@ function ansiCQuoteEnd(s, start) {
|
|
|
450
450
|
function backtickBody(body, dialect) {
|
|
451
451
|
return dialect.escape === "\\" ? body.replace(/\\([$`\\])/g, "$1") : body;
|
|
452
452
|
}
|
|
453
|
-
function skipHereDocuments(s, start, docs,
|
|
453
|
+
function skipHereDocuments(s, start, docs, nested2, dialect) {
|
|
454
454
|
let pos = start;
|
|
455
455
|
for (const doc of docs) {
|
|
456
456
|
const bodyStart = pos;
|
|
@@ -477,19 +477,19 @@ function skipHereDocuments(s, start, docs, nested, dialect) {
|
|
|
477
477
|
if (body[i] === "$" && body[i + 1] === "(") {
|
|
478
478
|
const close = matchParen(body, i + 1, dialect);
|
|
479
479
|
if (close < 0) return close;
|
|
480
|
-
|
|
480
|
+
nested2.push(body.slice(i + 2, close));
|
|
481
481
|
i = close;
|
|
482
482
|
} else if (body[i] === "`") {
|
|
483
483
|
const close = matchBacktick(body, i, dialect);
|
|
484
484
|
if (close < 0) return -1;
|
|
485
|
-
|
|
485
|
+
nested2.push(backtickBody(body.slice(i + 1, close), dialect));
|
|
486
486
|
i = close;
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
return pos;
|
|
491
491
|
}
|
|
492
|
-
function lex(command,
|
|
492
|
+
function lex(command, nested2, dialect) {
|
|
493
493
|
const segments = [];
|
|
494
494
|
let unsafeRedirect;
|
|
495
495
|
let processSubstitution = false;
|
|
@@ -503,6 +503,7 @@ function lex(command, nested, dialect) {
|
|
|
503
503
|
let bareSegment = false;
|
|
504
504
|
let tokens = [];
|
|
505
505
|
let expandable = false;
|
|
506
|
+
let globbed = false;
|
|
506
507
|
let current = "";
|
|
507
508
|
let started = false;
|
|
508
509
|
let piped = false;
|
|
@@ -566,7 +567,7 @@ function lex(command, nested, dialect) {
|
|
|
566
567
|
followable = false;
|
|
567
568
|
bareSegment = true;
|
|
568
569
|
}
|
|
569
|
-
segments.push({ ...seg, expandable, stdinRedirected, inputs, joinedBy });
|
|
570
|
+
segments.push({ ...seg, expandable, globbed, stdinRedirected, inputs, joinedBy });
|
|
570
571
|
} else if (!last || segments.length > 0 || joinedBy !== void 0) {
|
|
571
572
|
followable = false;
|
|
572
573
|
bareSegment = true;
|
|
@@ -574,6 +575,7 @@ function lex(command, nested, dialect) {
|
|
|
574
575
|
tokens = [];
|
|
575
576
|
inputs = [];
|
|
576
577
|
expandable = false;
|
|
578
|
+
globbed = false;
|
|
577
579
|
stdinRedirected = false;
|
|
578
580
|
piped = nextPiped;
|
|
579
581
|
joinedBy = next;
|
|
@@ -612,7 +614,7 @@ function lex(command, nested, dialect) {
|
|
|
612
614
|
unbalanced = true;
|
|
613
615
|
break;
|
|
614
616
|
}
|
|
615
|
-
|
|
617
|
+
nested2.push(command.slice(i + 2, close));
|
|
616
618
|
current += command.slice(i, close + 1);
|
|
617
619
|
started = true;
|
|
618
620
|
expandable = true;
|
|
@@ -625,7 +627,7 @@ function lex(command, nested, dialect) {
|
|
|
625
627
|
unbalanced = true;
|
|
626
628
|
break;
|
|
627
629
|
}
|
|
628
|
-
|
|
630
|
+
nested2.push(backtickBody(command.slice(i + 1, close), dialect));
|
|
629
631
|
current += command.slice(i, close + 1);
|
|
630
632
|
started = true;
|
|
631
633
|
expandable = true;
|
|
@@ -756,7 +758,7 @@ function lex(command, nested, dialect) {
|
|
|
756
758
|
endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
|
|
757
759
|
if (c === "\n") inComment = false;
|
|
758
760
|
if (c === "\n" && hereDocs.length > 0) {
|
|
759
|
-
const end = skipHereDocuments(command, i + 1, hereDocs.splice(0),
|
|
761
|
+
const end = skipHereDocuments(command, i + 1, hereDocs.splice(0), nested2, dialect);
|
|
760
762
|
if (end === UNREADABLE) {
|
|
761
763
|
unreadable = true;
|
|
762
764
|
break;
|
|
@@ -784,6 +786,7 @@ function lex(command, nested, dialect) {
|
|
|
784
786
|
else if (c === "{") braceOpen = true;
|
|
785
787
|
else if (braceOpen && (c === "," || c === "." && command[i + 1] === ".")) braceList = true;
|
|
786
788
|
else if (braceOpen && braceList && c === "}") expandable = true;
|
|
789
|
+
else if (c === "*" || c === "?" || c === "[") globbed = true;
|
|
787
790
|
current += c;
|
|
788
791
|
started = true;
|
|
789
792
|
i++;
|
|
@@ -819,8 +822,10 @@ function toSegment(tokens, piped, dialect) {
|
|
|
819
822
|
piped,
|
|
820
823
|
stdinRedirected: false,
|
|
821
824
|
expandable: false,
|
|
825
|
+
globbed: false,
|
|
822
826
|
inputs: [],
|
|
823
827
|
...first !== void 0 && isComputedWord(first, dialect) ? { computedBinary: first } : {},
|
|
828
|
+
...first !== void 0 && /[/\\]/.test(first) ? { binaryPath: first } : {},
|
|
824
829
|
...ambiguousCmdName !== void 0 ? { ambiguousCmdName } : {}
|
|
825
830
|
};
|
|
826
831
|
}
|
|
@@ -1109,7 +1114,7 @@ function unwrapXargs(seg, dialect) {
|
|
|
1109
1114
|
if (replace && tokens[0].includes(replace)) {
|
|
1110
1115
|
return { reason: `"xargs" substitutes its input into "${tokens[0]}", so the input decides which program runs. Name the program directly.` };
|
|
1111
1116
|
}
|
|
1112
|
-
return { seg: { ...toSegment(tokens, false, dialect), expandable: seg.expandable } };
|
|
1117
|
+
return { seg: { ...toSegment(tokens, false, dialect), expandable: seg.expandable, globbed: seg.globbed } };
|
|
1113
1118
|
}
|
|
1114
1119
|
function unwrap(seg, dialect) {
|
|
1115
1120
|
const wrappers = [];
|
|
@@ -1140,7 +1145,7 @@ function unwrap(seg, dialect) {
|
|
|
1140
1145
|
wrappers.push(current.binary);
|
|
1141
1146
|
const inner = toSegment(scan.tokens, current.piped, dialect);
|
|
1142
1147
|
assignments.push(...inner.assignments);
|
|
1143
|
-
current = { ...inner, assignments: [], expandable: current.expandable, stdinRedirected: current.stdinRedirected };
|
|
1148
|
+
current = { ...inner, assignments: [], expandable: current.expandable, globbed: current.globbed, stdinRedirected: current.stdinRedirected };
|
|
1144
1149
|
}
|
|
1145
1150
|
return { seg: { ...current, assignments }, wrappers, runner };
|
|
1146
1151
|
}
|
|
@@ -2991,6 +2996,340 @@ function awkProgramHazard(program) {
|
|
|
2991
2996
|
return void 0;
|
|
2992
2997
|
}
|
|
2993
2998
|
|
|
2999
|
+
// src/services/plannerAllowlist.ts
|
|
3000
|
+
var MUTATING_VERB = /^(abort|ack|activate|add|apply|approve|archive|assign|attach|bind|call|cancel|clear|clone|close|commit|copy|cp|create|deactivate|delete|deploy|destroy|detach|disable|download|drain|drop|dump|edit|enable|evict|exec|execute|expire|export|failover|flush|grant|import|insert|install|invoke|kill|label|lock|login|logout|merge|migrate|modify|move|mv|patch|pause|promote|publish|pull|purge|push|put|reboot|redeploy|register|reject|release|remove|rename|reopen|replace|request|reset|resize|restart|restore|resume|revoke|rm|rollback|rotate|run|scale|scp|send|set|ssh|start|stop|submit|suspend|sync|rsync|tag|taint|terminate|transfer|truncate|undelete|uninstall|unlock|unregister|unset|untag|update|upgrade|upload|write)([-_:.].*)?$/i;
|
|
3001
|
+
function globRegExp(glob, flags = "") {
|
|
3002
|
+
const escaped = glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
3003
|
+
return new RegExp(`^${escaped}$`, flags);
|
|
3004
|
+
}
|
|
3005
|
+
function ruleWord(pattern) {
|
|
3006
|
+
if (!pattern.includes("*")) return { matches: (word) => word === pattern, wildcard: false };
|
|
3007
|
+
const re = globRegExp(pattern);
|
|
3008
|
+
return { matches: (word) => re.test(word), wildcard: true };
|
|
3009
|
+
}
|
|
3010
|
+
function exclusionWord(pattern) {
|
|
3011
|
+
const re = /^-[A-Za-z]$/.test(pattern) ? new RegExp(`^-[A-Za-z]*${pattern[1]}`) : globRegExp(pattern, "i");
|
|
3012
|
+
return { matches: (word) => re.test(word), wildcard: pattern.includes("*") };
|
|
3013
|
+
}
|
|
3014
|
+
function isScopePattern(entry) {
|
|
3015
|
+
return /^(\/|~|https?:\/\/|mcp__|[A-Za-z]:[\\/])/.test(entry);
|
|
3016
|
+
}
|
|
3017
|
+
function parseAllowlist(entries) {
|
|
3018
|
+
const list = { commands: [], commandExclusions: [], tools: [], toolExclusions: [], scopes: [], scopeExclusions: [] };
|
|
3019
|
+
for (const raw of entries) {
|
|
3020
|
+
const exclude = raw.trim().startsWith("!");
|
|
3021
|
+
const entry = raw.trim().replace(/^!\s*/, "");
|
|
3022
|
+
if (!entry) continue;
|
|
3023
|
+
if (entry.startsWith("mcp:")) {
|
|
3024
|
+
(exclude ? list.toolExclusions : list.tools).push(globRegExp(entry.slice(4).trim(), "i"));
|
|
3025
|
+
} else if (isScopePattern(entry)) {
|
|
3026
|
+
(exclude ? list.scopeExclusions : list.scopes).push(globRegExp(entry));
|
|
3027
|
+
} else {
|
|
3028
|
+
const [binary, ...words] = entry.split(/\s+/);
|
|
3029
|
+
(exclude ? list.commandExclusions : list.commands).push({ text: raw.trim(), binary, words: words.map(exclude ? exclusionWord : ruleWord) });
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return list;
|
|
3033
|
+
}
|
|
3034
|
+
function leadingWords(args) {
|
|
3035
|
+
const lead = [];
|
|
3036
|
+
for (const arg of args) {
|
|
3037
|
+
if (arg.startsWith("-")) break;
|
|
3038
|
+
lead.push(arg);
|
|
3039
|
+
}
|
|
3040
|
+
return lead;
|
|
3041
|
+
}
|
|
3042
|
+
function ruleCovers(rule, command) {
|
|
3043
|
+
if (rule.binary !== command.binary) return false;
|
|
3044
|
+
const lead = leadingWords(command.args);
|
|
3045
|
+
if (lead.length < rule.words.length || rule.words.length < (command.scopeWords ?? 0)) return false;
|
|
3046
|
+
return rule.words.every((word, i) => word.matches(lead[i]) && !(word.wildcard && MUTATING_VERB.test(lead[i])));
|
|
3047
|
+
}
|
|
3048
|
+
function exclusionCovers(rule, command) {
|
|
3049
|
+
if (rule.binary !== "*" && rule.binary !== command.binary) return false;
|
|
3050
|
+
let next = 0;
|
|
3051
|
+
for (const arg of command.args) {
|
|
3052
|
+
if (next < rule.words.length && rule.words[next].matches(arg)) next += 1;
|
|
3053
|
+
}
|
|
3054
|
+
return next === rule.words.length;
|
|
3055
|
+
}
|
|
3056
|
+
function commandAllowedBy(list, command) {
|
|
3057
|
+
if (list.commandExclusions.some((rule) => exclusionCovers(rule, command))) return void 0;
|
|
3058
|
+
return list.commands.find((rule) => ruleCovers(rule, command))?.text;
|
|
3059
|
+
}
|
|
3060
|
+
function toolAllowed(list, tool2) {
|
|
3061
|
+
if (list.toolExclusions.some((re) => re.test(tool2))) return false;
|
|
3062
|
+
return list.tools.some((re) => re.test(tool2));
|
|
3063
|
+
}
|
|
3064
|
+
function scopeAllowed(list, scope) {
|
|
3065
|
+
if (list.scopeExclusions.some((re) => re.test(scope))) return false;
|
|
3066
|
+
return list.scopes.some((re) => re.test(scope)) || list.commands.some((rule) => rule.text === scope);
|
|
3067
|
+
}
|
|
3068
|
+
function withArgument(binary, verbs) {
|
|
3069
|
+
return verbs.flatMap((verb) => [`${binary} ${verb}`, `${binary} ${verb} *`]);
|
|
3070
|
+
}
|
|
3071
|
+
function nested(binary, verbs, depth) {
|
|
3072
|
+
return verbs.flatMap((verb) => Array.from({ length: depth }, (_, i) => [binary, ...Array(i).fill("*"), verb].join(" ")));
|
|
3073
|
+
}
|
|
3074
|
+
var KUBE_VERB_READS = ["get", "describe", "logs", "top", "explain", "events"];
|
|
3075
|
+
var KUBE_READS = [
|
|
3076
|
+
"api-resources",
|
|
3077
|
+
"api-versions",
|
|
3078
|
+
"version",
|
|
3079
|
+
"cluster-info",
|
|
3080
|
+
"auth can-i",
|
|
3081
|
+
"auth whoami",
|
|
3082
|
+
"config get-contexts",
|
|
3083
|
+
"config current-context",
|
|
3084
|
+
"config get-clusters",
|
|
3085
|
+
"rollout status",
|
|
3086
|
+
"rollout history"
|
|
3087
|
+
];
|
|
3088
|
+
var CONTAINER_VERB_READS = ["ps", "images", "inspect", "logs", "history", "top", "port"];
|
|
3089
|
+
var CONTAINER_READS = [
|
|
3090
|
+
"version",
|
|
3091
|
+
"info",
|
|
3092
|
+
"image ls",
|
|
3093
|
+
"image list",
|
|
3094
|
+
"image inspect",
|
|
3095
|
+
"image history",
|
|
3096
|
+
"container ls",
|
|
3097
|
+
"container list",
|
|
3098
|
+
"container inspect",
|
|
3099
|
+
"container logs",
|
|
3100
|
+
"network ls",
|
|
3101
|
+
"network inspect",
|
|
3102
|
+
"volume ls",
|
|
3103
|
+
"volume inspect",
|
|
3104
|
+
"context ls",
|
|
3105
|
+
"context list"
|
|
3106
|
+
];
|
|
3107
|
+
var DEFAULT_PLANNER_ALLOWLIST = [
|
|
3108
|
+
// GitHub, GitLab, Jira
|
|
3109
|
+
...[
|
|
3110
|
+
"issue list",
|
|
3111
|
+
"issue view",
|
|
3112
|
+
"issue status",
|
|
3113
|
+
"pr list",
|
|
3114
|
+
"pr view",
|
|
3115
|
+
"pr diff",
|
|
3116
|
+
"pr checks",
|
|
3117
|
+
"pr status",
|
|
3118
|
+
"run list",
|
|
3119
|
+
"run view",
|
|
3120
|
+
"workflow list",
|
|
3121
|
+
"workflow view",
|
|
3122
|
+
"repo view",
|
|
3123
|
+
"repo list",
|
|
3124
|
+
"search",
|
|
3125
|
+
"label list",
|
|
3126
|
+
"search *",
|
|
3127
|
+
"status",
|
|
3128
|
+
"cache list",
|
|
3129
|
+
"project list",
|
|
3130
|
+
"project view",
|
|
3131
|
+
"project item-list",
|
|
3132
|
+
"gist list",
|
|
3133
|
+
"gist view",
|
|
3134
|
+
"variable list",
|
|
3135
|
+
"ruleset list",
|
|
3136
|
+
"ruleset view",
|
|
3137
|
+
"org list"
|
|
3138
|
+
].map((c) => `gh ${c}`),
|
|
3139
|
+
...[
|
|
3140
|
+
"issue list",
|
|
3141
|
+
"issue view",
|
|
3142
|
+
"mr list",
|
|
3143
|
+
"mr view",
|
|
3144
|
+
"mr diff",
|
|
3145
|
+
"ci list",
|
|
3146
|
+
"ci status",
|
|
3147
|
+
"repo view",
|
|
3148
|
+
"release list",
|
|
3149
|
+
"release view"
|
|
3150
|
+
].map((c) => `glab ${c}`),
|
|
3151
|
+
...["issue list", "issue view", "sprint list", "epic list", "project list"].map((c) => `jira ${c}`),
|
|
3152
|
+
"!gh -w",
|
|
3153
|
+
"!glab -w",
|
|
3154
|
+
// Azure: `az` takes no positional arguments, so a group path is always groups then verb.
|
|
3155
|
+
...nested("az", ["list", "show"], 5),
|
|
3156
|
+
"az version",
|
|
3157
|
+
"az lock list",
|
|
3158
|
+
"az tag list",
|
|
3159
|
+
"az monitor log-analytics query",
|
|
3160
|
+
"az monitor app-insights query",
|
|
3161
|
+
"!az *keys*",
|
|
3162
|
+
"!az *secret*",
|
|
3163
|
+
"!az *credential*",
|
|
3164
|
+
"!az *password*",
|
|
3165
|
+
"!az *connection-string*",
|
|
3166
|
+
"!az *appsettings*",
|
|
3167
|
+
"!az *token*",
|
|
3168
|
+
// Google Cloud. `run` is spelled out: a wildcard never stands for it.
|
|
3169
|
+
...nested("gcloud", ["list", "describe", "get-iam-policy"], 5),
|
|
3170
|
+
"gcloud run * list",
|
|
3171
|
+
"gcloud run * * list",
|
|
3172
|
+
"gcloud run * describe",
|
|
3173
|
+
"gcloud run * * describe",
|
|
3174
|
+
"gcloud config list",
|
|
3175
|
+
"gcloud config get",
|
|
3176
|
+
"gcloud config get-value",
|
|
3177
|
+
"gcloud auth list",
|
|
3178
|
+
"gcloud logging read",
|
|
3179
|
+
"gcloud info",
|
|
3180
|
+
"gcloud version",
|
|
3181
|
+
...["ls", "du"].map((c) => `gsutil ${c}`),
|
|
3182
|
+
...["ls", "show", "head"].map((c) => `bq ${c}`),
|
|
3183
|
+
"!gcloud *secret*",
|
|
3184
|
+
"!gcloud *token*",
|
|
3185
|
+
// AWS: always `aws <service> <operation>`.
|
|
3186
|
+
"aws * describe-*",
|
|
3187
|
+
"aws * list-*",
|
|
3188
|
+
"aws s3 ls",
|
|
3189
|
+
"aws sts get-caller-identity",
|
|
3190
|
+
"aws configure list",
|
|
3191
|
+
"aws logs filter-log-events",
|
|
3192
|
+
"aws logs get-log-events",
|
|
3193
|
+
"aws cloudwatch get-metric-data",
|
|
3194
|
+
"aws cloudwatch get-metric-statistics",
|
|
3195
|
+
"aws ce get-cost-and-usage",
|
|
3196
|
+
"aws iam get-user",
|
|
3197
|
+
"aws iam get-role",
|
|
3198
|
+
"!aws secretsmanager",
|
|
3199
|
+
"!aws *secret*",
|
|
3200
|
+
// Other clouds and platforms
|
|
3201
|
+
...nested("doctl", ["list", "get"], 3),
|
|
3202
|
+
"doctl account get",
|
|
3203
|
+
"!doctl -u",
|
|
3204
|
+
...nested("hcloud", ["list", "describe"], 2),
|
|
3205
|
+
...nested("linode-cli", ["list", "view"], 2),
|
|
3206
|
+
...nested("oci", ["list"], 4),
|
|
3207
|
+
...[
|
|
3208
|
+
"status",
|
|
3209
|
+
"releases",
|
|
3210
|
+
"apps list",
|
|
3211
|
+
"machine list",
|
|
3212
|
+
"machines list",
|
|
3213
|
+
"secrets list",
|
|
3214
|
+
"regions list",
|
|
3215
|
+
"ips list",
|
|
3216
|
+
"volumes list"
|
|
3217
|
+
].flatMap((c) => [`fly ${c}`, `flyctl ${c}`]),
|
|
3218
|
+
...["apps", "apps:info", "ps", "logs", "releases", "addons", "domains", "pipelines"].map((c) => `heroku ${c}`),
|
|
3219
|
+
...["ls", "list", "inspect", "logs", "env ls", "domains ls", "project ls", "whoami"].map((c) => `vercel ${c}`),
|
|
3220
|
+
"netlify status",
|
|
3221
|
+
"netlify sites:list",
|
|
3222
|
+
"firebase projects:list",
|
|
3223
|
+
"firebase apps:list",
|
|
3224
|
+
"firebase hosting:channel:list",
|
|
3225
|
+
"wrangler whoami",
|
|
3226
|
+
"wrangler deployments list",
|
|
3227
|
+
"wrangler d1 list",
|
|
3228
|
+
"wrangler kv namespace list",
|
|
3229
|
+
"supabase projects list",
|
|
3230
|
+
"supabase functions list",
|
|
3231
|
+
"railway status",
|
|
3232
|
+
// Kubernetes and friends. A kubeconfig the command names can run a credential plugin; `--raw` reads any API path.
|
|
3233
|
+
...withArgument("kubectl", KUBE_VERB_READS),
|
|
3234
|
+
...KUBE_READS.map((c) => `kubectl ${c}`),
|
|
3235
|
+
...[...KUBE_VERB_READS, ...KUBE_READS, "status", "whoami", "projects"].map((c) => `oc ${c}`),
|
|
3236
|
+
...["kubectl", "oc"].flatMap((bin) => ["*secret*", "-s", "dump", "--raw*", "-k", "--kustomize*"].map((w) => `!${bin} ${w}`)),
|
|
3237
|
+
"!oc whoami -t",
|
|
3238
|
+
"!oc *show-token*",
|
|
3239
|
+
...withArgument("helm", ["list", "ls", "status", "history", "search"]),
|
|
3240
|
+
"helm version",
|
|
3241
|
+
"!helm --kube*",
|
|
3242
|
+
"eksctl get",
|
|
3243
|
+
"kind get",
|
|
3244
|
+
"k3d cluster list",
|
|
3245
|
+
"minikube status",
|
|
3246
|
+
"minikube profile list",
|
|
3247
|
+
"!kind *kubeconfig*",
|
|
3248
|
+
"argocd app list",
|
|
3249
|
+
"argocd app get",
|
|
3250
|
+
"argocd app history",
|
|
3251
|
+
"istioctl analyze",
|
|
3252
|
+
"istioctl proxy-status",
|
|
3253
|
+
"istioctl version",
|
|
3254
|
+
"!istioctl -c",
|
|
3255
|
+
"nomad status",
|
|
3256
|
+
"nomad job status",
|
|
3257
|
+
"consul members",
|
|
3258
|
+
"consul catalog services",
|
|
3259
|
+
"vault status",
|
|
3260
|
+
// Containers. Not `compose`, which reads the repository's compose file, remote includes and all.
|
|
3261
|
+
...["docker", "podman"].flatMap((bin) => [...withArgument(bin, CONTAINER_VERB_READS), ...CONTAINER_READS.map((c) => `${bin} ${c}`), `!${bin} -H`]),
|
|
3262
|
+
// Infrastructure as code: only what reads no backend, whose address the repository's config sets.
|
|
3263
|
+
...["version", "workspace show"].flatMap((c) => [`terraform ${c}`, `tofu ${c}`]),
|
|
3264
|
+
// Package metadata from what is installed: npm reads `node_modules`, pip its own environment.
|
|
3265
|
+
...withArgument("npm", ["ls", "list", "why", "explain"]),
|
|
3266
|
+
...["pip", "pip3"].flatMap((bin) => [...withArgument(bin, ["show"]), `${bin} list`, `${bin} freeze`, `!${bin} -i`, `!${bin} -f`]),
|
|
3267
|
+
// A module query (`@latest`) fetches from whatever host the module path names.
|
|
3268
|
+
...withArgument("go", ["list", "env"]),
|
|
3269
|
+
"go version",
|
|
3270
|
+
"go mod graph",
|
|
3271
|
+
"go mod why",
|
|
3272
|
+
"!go -w",
|
|
3273
|
+
"!go -u",
|
|
3274
|
+
"!go -mod*",
|
|
3275
|
+
"!go *-toolexec*",
|
|
3276
|
+
"!go *-exec*",
|
|
3277
|
+
"!go *-compiled*",
|
|
3278
|
+
"!go *-export*",
|
|
3279
|
+
"!go *@*",
|
|
3280
|
+
// The local machine
|
|
3281
|
+
"ps",
|
|
3282
|
+
"lsof",
|
|
3283
|
+
"uptime",
|
|
3284
|
+
"free",
|
|
3285
|
+
"nproc",
|
|
3286
|
+
"hostname",
|
|
3287
|
+
"id",
|
|
3288
|
+
"journalctl",
|
|
3289
|
+
"!journalctl --vacuum*",
|
|
3290
|
+
"!journalctl --rotate",
|
|
3291
|
+
"!journalctl --flush",
|
|
3292
|
+
"!journalctl --sync",
|
|
3293
|
+
"!journalctl --relinquish*",
|
|
3294
|
+
"!journalctl --smart-relinquish*",
|
|
3295
|
+
"!journalctl --setup-keys",
|
|
3296
|
+
"!journalctl --update-catalog",
|
|
3297
|
+
"!hostname *",
|
|
3298
|
+
// Anything that would write a file, open a browser, or send the user's credentials to an address the command names.
|
|
3299
|
+
...[
|
|
3300
|
+
"--output-file*",
|
|
3301
|
+
"--out-file*",
|
|
3302
|
+
"--outfile*",
|
|
3303
|
+
"--output-document*",
|
|
3304
|
+
"--output-directory*",
|
|
3305
|
+
"--log-file*",
|
|
3306
|
+
"--logfile*",
|
|
3307
|
+
"--download*",
|
|
3308
|
+
"--show-secrets*",
|
|
3309
|
+
"--with-decryption*",
|
|
3310
|
+
"--reveal*",
|
|
3311
|
+
"--web",
|
|
3312
|
+
"--server*",
|
|
3313
|
+
"*-addr*",
|
|
3314
|
+
"--endpoint*",
|
|
3315
|
+
"--api-url*",
|
|
3316
|
+
"--url*",
|
|
3317
|
+
"--host*",
|
|
3318
|
+
"--registry*",
|
|
3319
|
+
"--index-url*",
|
|
3320
|
+
"--extra-index-url*",
|
|
3321
|
+
"--find-links*",
|
|
3322
|
+
"--proxy*",
|
|
3323
|
+
"--kubeconfig*",
|
|
3324
|
+
"*apiserver*",
|
|
3325
|
+
"*http://*",
|
|
3326
|
+
"*https://*"
|
|
3327
|
+
].map((w) => `!* ${w}`),
|
|
3328
|
+
// MCP tools that read: the verb leads the tool's own name.
|
|
3329
|
+
...["get*", "list*", "find*", "search*", "read*", "view*", "describe*", "show*"].map((v) => `mcp:${v}`),
|
|
3330
|
+
...["*secret*", "*token*", "*password*", "*credential*", "*key*", "*url*", "*fetch*", "*http*"].map((w) => `!mcp:${w}`)
|
|
3331
|
+
];
|
|
3332
|
+
|
|
2994
3333
|
// src/services/commandPolicy.ts
|
|
2995
3334
|
var AUTO_COMMANDS = [
|
|
2996
3335
|
"ls",
|
|
@@ -3424,18 +3763,20 @@ function patternArgs(seg) {
|
|
|
3424
3763
|
}
|
|
3425
3764
|
var SCOPE_LEAD_ARGS = 2;
|
|
3426
3765
|
function scopeFor(seg) {
|
|
3427
|
-
|
|
3766
|
+
const name = seg.binaryPath ?? seg.binary;
|
|
3767
|
+
if (!MULTIPLEXERS.includes(seg.binary.toLowerCase())) return name;
|
|
3428
3768
|
const lead = [];
|
|
3429
3769
|
for (const arg of seg.args) {
|
|
3430
3770
|
if (arg.startsWith("-")) break;
|
|
3431
3771
|
lead.push(arg);
|
|
3432
3772
|
if (lead.length === SCOPE_LEAD_ARGS) break;
|
|
3433
3773
|
}
|
|
3434
|
-
return [
|
|
3774
|
+
return [name, ...lead].join(" ");
|
|
3435
3775
|
}
|
|
3436
3776
|
function isAuto(seg, opts) {
|
|
3437
3777
|
if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
|
|
3438
3778
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
3779
|
+
if (seg.binaryPath !== void 0) return false;
|
|
3439
3780
|
if (seg.expandable) return false;
|
|
3440
3781
|
if (seg.binary !== "git") return true;
|
|
3441
3782
|
const sub = subcommandOf(seg);
|
|
@@ -3622,7 +3963,18 @@ function classifyCommand(command, opts = {}) {
|
|
|
3622
3963
|
return { tier: "ask", scope: trimmed };
|
|
3623
3964
|
}
|
|
3624
3965
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3625
|
-
|
|
3966
|
+
const allowedBy = opts.allow ? allowlistRule(nonAuto, opts.allow) : void 0;
|
|
3967
|
+
return { tier: "ask", scope, ...allowedBy ? { allowedBy } : {} };
|
|
3968
|
+
}
|
|
3969
|
+
function allowlistRule(parts, allow) {
|
|
3970
|
+
const rules = [];
|
|
3971
|
+
for (const { seg, runner } of parts) {
|
|
3972
|
+
if (runner || seg.expandable || seg.globbed || seg.binaryPath !== void 0 || seg.assignments.length > 0) return void 0;
|
|
3973
|
+
const rule = commandAllowedBy(allow, { binary: seg.binary, args: seg.args, scopeWords: scopeFor(seg).split(" ").length - 1 });
|
|
3974
|
+
if (!rule) return void 0;
|
|
3975
|
+
rules.push(rule);
|
|
3976
|
+
}
|
|
3977
|
+
return [...new Set(rules)].join(" + ");
|
|
3626
3978
|
}
|
|
3627
3979
|
|
|
3628
3980
|
// src/services/researchShell.ts
|
|
@@ -3938,7 +4290,8 @@ ${refs.output.trim()}`);
|
|
|
3938
4290
|
* directory the next command names.
|
|
3939
4291
|
*/
|
|
3940
4292
|
async bash(command, signal) {
|
|
3941
|
-
const
|
|
4293
|
+
const allow = this.approval.allowlist?.();
|
|
4294
|
+
const { tier, scope, reason, allowedBy } = classifyCommand(command, { ...this.policyOptions(), ...allow ? { allow } : {} });
|
|
3942
4295
|
if (tier === "refuse") {
|
|
3943
4296
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
3944
4297
|
}
|
|
@@ -3953,13 +4306,14 @@ ${refs.output.trim()}`);
|
|
|
3953
4306
|
subject: asks ? command : outside[0].abs,
|
|
3954
4307
|
scope: scopes[0],
|
|
3955
4308
|
...scopes.length > 1 ? { scopes } : {},
|
|
4309
|
+
...asks && allowedBy ? { allowedBy } : {},
|
|
3956
4310
|
detail: outside.length > 0 ? `Planner research wants to run "${command}", which touches ${touched}, outside the workspace (${root}).` : `Planner research wants to run: ${command}`
|
|
3957
4311
|
});
|
|
3958
4312
|
if (!granted) {
|
|
3959
4313
|
return {
|
|
3960
4314
|
success: false,
|
|
3961
4315
|
output: outside.length > 0 ? `Access denied: "${command}" touches ${touched}, outside the workspace root (${root}), and was not approved. Keep research inside the workspace, or ask the user to approve this location.` : `Command not approved: ${command}
|
|
3962
|
-
It is outside the auto-allowed read-only set (${scope}). Continue
|
|
4316
|
+
It is outside the auto-allowed read-only set and the planner allowlist (${scope}). Continue without it. If it would change something outside the repository, add it to the plan as an ops task; if it only reads, ask the user to approve it or add it to the planner allowlist.`,
|
|
3963
4317
|
truncated: false
|
|
3964
4318
|
};
|
|
3965
4319
|
}
|
|
@@ -4187,14 +4541,19 @@ async function runTool(name, args, fs17, fetcher, signal) {
|
|
|
4187
4541
|
}
|
|
4188
4542
|
|
|
4189
4543
|
// src/services/ApprovalPolicy.ts
|
|
4190
|
-
function
|
|
4191
|
-
if (
|
|
4192
|
-
|
|
4193
|
-
|
|
4544
|
+
function effectiveApprovalMode(setting, autonomous) {
|
|
4545
|
+
if (setting !== "auto") return setting;
|
|
4546
|
+
return autonomous ? "deny" : "ask";
|
|
4547
|
+
}
|
|
4548
|
+
function parseApprovalModeSetting(raw) {
|
|
4549
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
4550
|
+
if (value === "allowlist") return "deny";
|
|
4551
|
+
return value === "ask" || value === "allow" || value === "deny" ? value : "auto";
|
|
4194
4552
|
}
|
|
4195
4553
|
var ApprovalPolicy = class {
|
|
4196
|
-
|
|
4197
|
-
|
|
4554
|
+
modeOf;
|
|
4555
|
+
entriesOf;
|
|
4556
|
+
parsed = null;
|
|
4198
4557
|
asker;
|
|
4199
4558
|
onDecision;
|
|
4200
4559
|
granted = /* @__PURE__ */ new Set();
|
|
@@ -4206,8 +4565,9 @@ var ApprovalPolicy = class {
|
|
|
4206
4565
|
// generation it was started under and bails if reset has intervened.
|
|
4207
4566
|
generation = 0;
|
|
4208
4567
|
constructor(opts = {}) {
|
|
4209
|
-
|
|
4210
|
-
this.
|
|
4568
|
+
const { mode = "ask", preApproved = [] } = opts;
|
|
4569
|
+
this.modeOf = typeof mode === "function" ? mode : () => mode;
|
|
4570
|
+
this.entriesOf = typeof preApproved === "function" ? preApproved : () => preApproved;
|
|
4211
4571
|
this.asker = opts.ask;
|
|
4212
4572
|
this.onDecision = opts.onDecision;
|
|
4213
4573
|
}
|
|
@@ -4218,19 +4578,19 @@ var ApprovalPolicy = class {
|
|
|
4218
4578
|
};
|
|
4219
4579
|
const scopes = approvalScopes(req);
|
|
4220
4580
|
const key = scopes.join("\n");
|
|
4221
|
-
|
|
4222
|
-
const
|
|
4581
|
+
const list = this.allowlist();
|
|
4582
|
+
const preApproved = (scope, i) => scopeAllowed(list, scope) || i === 0 && (req.kind === "shell_command" && req.allowedBy !== void 0 || req.kind === "mcp_tool" && req.tool !== void 0 && toolAllowed(list, req.tool));
|
|
4583
|
+
const open = scopes.filter((s, i) => !this.granted.has(s) && !preApproved(s, i));
|
|
4223
4584
|
if (open.length === 0) {
|
|
4224
4585
|
return decide(true, scopes.every((s) => this.granted.has(s)) ? "remembered" : "pre-approved");
|
|
4225
4586
|
}
|
|
4226
|
-
if (this.
|
|
4587
|
+
if (scopes.some((s) => this.refused.has(s)) || this.refused.has(key)) return decide(false, "remembered");
|
|
4588
|
+
const mode = this.modeOf();
|
|
4589
|
+
if (mode === "allow") {
|
|
4227
4590
|
open.forEach((s) => this.granted.add(s));
|
|
4228
4591
|
return decide(true, "mode");
|
|
4229
4592
|
}
|
|
4230
|
-
if (
|
|
4231
|
-
this.refused.add(key);
|
|
4232
|
-
return decide(false, "mode");
|
|
4233
|
-
}
|
|
4593
|
+
if (mode === "deny") return decide(false, "mode");
|
|
4234
4594
|
if (!this.asker) return decide(false, "no-channel");
|
|
4235
4595
|
const openKey = open.join("\n");
|
|
4236
4596
|
const existing = this.inFlight.get(openKey);
|
|
@@ -4248,6 +4608,13 @@ var ApprovalPolicy = class {
|
|
|
4248
4608
|
}
|
|
4249
4609
|
return decide(answer2, "asked");
|
|
4250
4610
|
}
|
|
4611
|
+
/** The standing approvals, parsed from the entries in force now. */
|
|
4612
|
+
allowlist() {
|
|
4613
|
+
const entries = this.entriesOf();
|
|
4614
|
+
const key = entries.join("\n");
|
|
4615
|
+
if (this.parsed?.key !== key) this.parsed = { key, list: parseAllowlist(entries) };
|
|
4616
|
+
return this.parsed.list;
|
|
4617
|
+
}
|
|
4251
4618
|
/** Scopes the user has granted this session — for display and for persistence. */
|
|
4252
4619
|
grantedScopes() {
|
|
4253
4620
|
return [...this.granted];
|
|
@@ -5158,7 +5525,7 @@ function configuredProviders(config) {
|
|
|
5158
5525
|
}
|
|
5159
5526
|
|
|
5160
5527
|
// src/utils/maxParallel.ts
|
|
5161
|
-
var DEFAULT_MAX_PARALLEL =
|
|
5528
|
+
var DEFAULT_MAX_PARALLEL = 5;
|
|
5162
5529
|
function parseMaxParallel(value) {
|
|
5163
5530
|
const text = String(value ?? "").trim();
|
|
5164
5531
|
const n = typeof value === "number" ? value : Number(text);
|
|
@@ -5403,8 +5770,10 @@ var BaseConfig = class {
|
|
|
5403
5770
|
return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
|
|
5404
5771
|
}
|
|
5405
5772
|
get approvalMode() {
|
|
5406
|
-
|
|
5407
|
-
|
|
5773
|
+
return parseApprovalModeSetting(process.env.ORDEWELL_APPROVAL_MODE);
|
|
5774
|
+
}
|
|
5775
|
+
get approvalDefaults() {
|
|
5776
|
+
return process.env.ORDEWELL_APPROVAL_DEFAULTS !== "false" && process.env.ORDEWELL_APPROVAL_DEFAULTS !== "0";
|
|
5408
5777
|
}
|
|
5409
5778
|
get approvalPreApproved() {
|
|
5410
5779
|
return (process.env.ORDEWELL_APPROVAL_ALLOW || "").split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
|
|
@@ -6144,7 +6513,7 @@ var GitWorktreeIsolation = class {
|
|
|
6144
6513
|
return this.admin(workspaceRoot, async () => {
|
|
6145
6514
|
const scan = await this.scanGroup(workspaceRoot);
|
|
6146
6515
|
const paths = "paths" in scan ? scan.paths : [SELF_REPO];
|
|
6147
|
-
const
|
|
6516
|
+
const nested2 = "paths" in scan ? scan.nested : [];
|
|
6148
6517
|
const run = { id: this.mintRunId(), workspaceRoot, repos: [], shared: [], sharedRepos: [], tasks: {} };
|
|
6149
6518
|
for (const repoPath of paths) {
|
|
6150
6519
|
const repo = await this.startRepo(run, repoPath);
|
|
@@ -6152,7 +6521,7 @@ var GitWorktreeIsolation = class {
|
|
|
6152
6521
|
else run.sharedRepos.push(repoPath);
|
|
6153
6522
|
}
|
|
6154
6523
|
if (run.repos.length === 0) throw new Error(`No repository could be isolated: ${run.sharedRepos.join(", ")}`);
|
|
6155
|
-
run.shared = paths.includes(SELF_REPO) ?
|
|
6524
|
+
run.shared = paths.includes(SELF_REPO) ? nested2 : this.sharedPaths(workspaceRoot, run.repos.map((r) => r.path));
|
|
6156
6525
|
return run;
|
|
6157
6526
|
});
|
|
6158
6527
|
}
|
|
@@ -7435,6 +7804,7 @@ function researchPhaseBlock(harnessMode) {
|
|
|
7435
7804
|
const toolSpecific = harnessMode ? [
|
|
7436
7805
|
"- Explore the repository with your own tools until you understand its architecture",
|
|
7437
7806
|
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
7807
|
+
"- Run commands \u2014 git, gh, cloud CLIs, the project's own tooling \u2014 with the run_command tool, not a shell of your own. MCP tools the user configured (issue trackers, todo lists) are yours to read from too. Reads run at once; anything else is approved or refused by the user's settings. When something the plan needs to change is refused, add it to the plan as an ops task instead",
|
|
7438
7808
|
"- If you delegate exploration to your own agents, WAIT for their results inside this reply. Do NOT launch them in the background or async and end your turn saying you will report back later: your turn ending is what hands the conversation back to the user, and anything you say after it never reaches them."
|
|
7439
7809
|
] : [
|
|
7440
7810
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
@@ -9744,6 +10114,9 @@ var listRunnersInput = z.object({});
|
|
|
9744
10114
|
var listModelsInput = z.object({
|
|
9745
10115
|
runner: z.string().min(1).describe("A runner id from list_runners.")
|
|
9746
10116
|
});
|
|
10117
|
+
var runCommandInput = z.object({
|
|
10118
|
+
command: z.string().min(1).describe("One shell command line, run from the workspace root.")
|
|
10119
|
+
});
|
|
9747
10120
|
var loadSkillInput = z.object({
|
|
9748
10121
|
name: z.string().min(1).describe("The name of an Ordewell planner skill to load.")
|
|
9749
10122
|
});
|
|
@@ -9883,6 +10256,13 @@ var PLANNER_TOOLS = [
|
|
|
9883
10256
|
(h) => h.taskOutput?.bind(h),
|
|
9884
10257
|
PLANNER_READ_ONLY
|
|
9885
10258
|
),
|
|
10259
|
+
tool(
|
|
10260
|
+
"run_command",
|
|
10261
|
+
"Run a shell command for research: git, gh, az, kubectl, the project's own tooling, anything you need to look at. Read-only commands run at once; others ask the user, once per command family; commands that write files are refused. Use it instead of a shell of your own.",
|
|
10262
|
+
runCommandInput,
|
|
10263
|
+
(h) => h.runCommand?.bind(h),
|
|
10264
|
+
PLANNER_READ_ONLY
|
|
10265
|
+
),
|
|
9886
10266
|
tool(
|
|
9887
10267
|
"load_skill",
|
|
9888
10268
|
"Load an Ordewell planner skill by name. Returns its instructions; only model-invocable planner skills can be loaded.",
|
|
@@ -12370,6 +12750,7 @@ var SETTINGS_ENV_REFUSED = [
|
|
|
12370
12750
|
"ORDEWELL_AUTONOMOUS_MODE",
|
|
12371
12751
|
"ORDEWELL_APPROVAL_MODE",
|
|
12372
12752
|
"ORDEWELL_APPROVAL_ALLOW",
|
|
12753
|
+
"ORDEWELL_APPROVAL_DEFAULTS",
|
|
12373
12754
|
// Where settings are persisted.
|
|
12374
12755
|
"ORDEWELL_SETTINGS_PATH",
|
|
12375
12756
|
// Runtime options and dynamic loaders.
|
|
@@ -12968,6 +13349,32 @@ function killTree(proc, deps = {}) {
|
|
|
12968
13349
|
// src/services/harness/openCodeOrdewell.ts
|
|
12969
13350
|
var NAMES = prefixedToolNames(`${ORDEWELL_MCP_SERVER_NAME}_`);
|
|
12970
13351
|
var RULE = NAMES.toolName("*");
|
|
13352
|
+
var OPENCODE_PLANNER_PERMISSION = {
|
|
13353
|
+
"*": "ask",
|
|
13354
|
+
read: "allow",
|
|
13355
|
+
glob: "allow",
|
|
13356
|
+
grep: "allow",
|
|
13357
|
+
list: "allow",
|
|
13358
|
+
lsp: "allow",
|
|
13359
|
+
skill: "allow",
|
|
13360
|
+
todoread: "allow",
|
|
13361
|
+
websearch: "allow",
|
|
13362
|
+
codesearch: "allow",
|
|
13363
|
+
webfetch: "ask",
|
|
13364
|
+
external_directory: "ask",
|
|
13365
|
+
edit: "deny",
|
|
13366
|
+
write: "deny",
|
|
13367
|
+
patch: "deny",
|
|
13368
|
+
apply_patch: "deny",
|
|
13369
|
+
multiedit: "deny",
|
|
13370
|
+
bash: "deny",
|
|
13371
|
+
task: "deny",
|
|
13372
|
+
question: "deny",
|
|
13373
|
+
todowrite: "deny",
|
|
13374
|
+
shell: "deny",
|
|
13375
|
+
subagent: "deny",
|
|
13376
|
+
browser: "deny"
|
|
13377
|
+
};
|
|
12971
13378
|
var OPENCODE_CONFIG_VARIABLE = "OPENCODE_CONFIG_CONTENT";
|
|
12972
13379
|
function isRecord(value) {
|
|
12973
13380
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -13005,7 +13412,7 @@ function withoutParentOrdewell(content) {
|
|
|
13005
13412
|
const kept = withoutOrdewell(parsed);
|
|
13006
13413
|
return JSON.stringify(kept) === JSON.stringify(parsed) ? content : JSON.stringify(kept);
|
|
13007
13414
|
}
|
|
13008
|
-
function mergeOrdewellConfig(existing, mcp) {
|
|
13415
|
+
function mergeOrdewellConfig(existing, mcp, role) {
|
|
13009
13416
|
let base = {};
|
|
13010
13417
|
if (existing?.trim()) {
|
|
13011
13418
|
let parsed;
|
|
@@ -13017,10 +13424,14 @@ function mergeOrdewellConfig(existing, mcp) {
|
|
|
13017
13424
|
if (!isRecord(parsed)) return null;
|
|
13018
13425
|
base = withoutOrdewell(parsed);
|
|
13019
13426
|
}
|
|
13427
|
+
if (role === "planner") {
|
|
13428
|
+
delete base.permission;
|
|
13429
|
+
delete base.permissions;
|
|
13430
|
+
}
|
|
13020
13431
|
const policy = typeof base.permission === "string" ? { "*": base.permission } : base.permission;
|
|
13021
13432
|
const ours = {
|
|
13022
|
-
mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } },
|
|
13023
|
-
permission: { [RULE]: "allow" }
|
|
13433
|
+
...mcp ? { mcp: { [mcp.name]: { type: "remote", url: mcp.url, headers: mcp.headers, enabled: true } } } : {},
|
|
13434
|
+
permission: { ...role === "planner" ? OPENCODE_PLANNER_PERMISSION : {}, ...mcp ? { [RULE]: "allow" } : {} }
|
|
13024
13435
|
};
|
|
13025
13436
|
return JSON.stringify(deepMerge({ ...base, ...policy === void 0 ? {} : { permission: policy } }, ours));
|
|
13026
13437
|
}
|
|
@@ -13465,7 +13876,24 @@ var DISALLOWED_TOOLS = [
|
|
|
13465
13876
|
"EnterPlanMode",
|
|
13466
13877
|
"ExitPlanMode"
|
|
13467
13878
|
];
|
|
13468
|
-
var PLANNER_PERMISSION_MODE = "
|
|
13879
|
+
var PLANNER_PERMISSION_MODE = "default";
|
|
13880
|
+
var PATH_TOOLS = {
|
|
13881
|
+
Read: { field: "file_path", directory: false },
|
|
13882
|
+
Grep: { field: "path", directory: true },
|
|
13883
|
+
Glob: { field: "path", directory: true },
|
|
13884
|
+
LS: { field: "path", directory: true }
|
|
13885
|
+
};
|
|
13886
|
+
function plannerAsk(name, input, server) {
|
|
13887
|
+
if (name.startsWith("mcp__")) {
|
|
13888
|
+
const tool2 = name.split("__").slice(2).join("__");
|
|
13889
|
+
return { kind: "mcp", scope: name, tool: tool2 || name, ...server ? { server } : {} };
|
|
13890
|
+
}
|
|
13891
|
+
if (name === "WebFetch" && typeof input.url === "string") return { kind: "fetch", url: input.url };
|
|
13892
|
+
const pathTool = PATH_TOOLS[name];
|
|
13893
|
+
const target = pathTool ? input[pathTool.field] : void 0;
|
|
13894
|
+
if (pathTool && typeof target === "string") return { kind: "path", path: target, directory: pathTool.directory };
|
|
13895
|
+
return { kind: "other" };
|
|
13896
|
+
}
|
|
13469
13897
|
var TASK_DISALLOWED_TOOLS = ["AskUserQuestion"];
|
|
13470
13898
|
var PROTOCOL_ARGS = [
|
|
13471
13899
|
"-p",
|
|
@@ -13575,6 +14003,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
13575
14003
|
const args = [
|
|
13576
14004
|
...PROTOCOL_ARGS,
|
|
13577
14005
|
// The read-only guarantee, enforced at spawn rather than by prompt.
|
|
14006
|
+
"--permission-prompt-tool",
|
|
14007
|
+
"stdio",
|
|
13578
14008
|
"--permission-mode",
|
|
13579
14009
|
PLANNER_PERMISSION_MODE,
|
|
13580
14010
|
"--disallowedTools",
|
|
@@ -13761,32 +14191,20 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
13761
14191
|
const subagentId = msg.parent_tool_use_id ?? void 0;
|
|
13762
14192
|
switch (msg.type) {
|
|
13763
14193
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
13764
|
-
// cannot decide alone.
|
|
13765
|
-
//
|
|
13766
|
-
//
|
|
14194
|
+
// cannot decide alone. The request is left open — a task's for someone
|
|
14195
|
+
// to answer (ADR-0018, A1), a planner's for its envelope (ADR-0026) —
|
|
14196
|
+
// and must be answered, because an unacknowledged request stalls the turn.
|
|
13767
14197
|
case "control_request": {
|
|
13768
14198
|
if (msg.request?.subtype !== "can_use_tool") return;
|
|
13769
14199
|
const input = msg.request.input ?? {};
|
|
13770
14200
|
const id = msg.request_id ?? "";
|
|
13771
|
-
if (this.role === "planner") {
|
|
13772
|
-
this.writeLine({
|
|
13773
|
-
type: "control_response",
|
|
13774
|
-
response: {
|
|
13775
|
-
subtype: "success",
|
|
13776
|
-
request_id: msg.request_id,
|
|
13777
|
-
response: { behavior: "deny", message: "The Ordewell planner is read-only. Mutation belongs to the runners that execute the plan." }
|
|
13778
|
-
}
|
|
13779
|
-
});
|
|
13780
|
-
emit({ type: "permission_request", id, name: msg.request.tool_name ?? "unknown", detail: JSON.stringify(input) });
|
|
13781
|
-
return;
|
|
13782
|
-
}
|
|
13783
14201
|
const name = msg.request.tool_name ?? "unknown";
|
|
13784
14202
|
if (this.givenOrdewell && CLAUDE_ORDEWELL.isOrdewellAsk(name)) {
|
|
13785
14203
|
this.writeLine({ type: "control_response", response: { subtype: "success", request_id: id, response: { behavior: "allow", updatedInput: input } } });
|
|
13786
14204
|
emit({ type: "permission_request", id, name, detail: JSON.stringify(input), input, decided: { decision: "allow" } });
|
|
13787
14205
|
return;
|
|
13788
14206
|
}
|
|
13789
|
-
const suggestions = msg.request.permission_suggestions ?? [];
|
|
14207
|
+
const suggestions = this.role === "task" ? msg.request.permission_suggestions ?? [] : [];
|
|
13790
14208
|
this.openPermissions.set(id, { input, suggestions });
|
|
13791
14209
|
emit({
|
|
13792
14210
|
type: "permission_request",
|
|
@@ -13795,7 +14213,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
13795
14213
|
detail: JSON.stringify(input),
|
|
13796
14214
|
input,
|
|
13797
14215
|
suggestions,
|
|
13798
|
-
...msg.request.tool_use_id ? { toolUseId: msg.request.tool_use_id } : {}
|
|
14216
|
+
...msg.request.tool_use_id ? { toolUseId: msg.request.tool_use_id } : {},
|
|
14217
|
+
...this.role === "planner" ? { ask: plannerAsk(name, input, msg.request.mcp_server?.name) } : {}
|
|
13799
14218
|
});
|
|
13800
14219
|
return;
|
|
13801
14220
|
}
|
|
@@ -14087,6 +14506,20 @@ var CODEX_ORDEWELL = {
|
|
|
14087
14506
|
`They are not in your tool list up front. Find them with the tool discovery you have (the \`exec\` tool's \`ALL_TOOLS\` list) and call them by their full names, ${quoted2(this.toolNames("task"))}.`,
|
|
14088
14507
|
`Look for them before you finish; the task is reported complete through \`${TASK_COMPLETE_TOOL}\`.`
|
|
14089
14508
|
].join(" ");
|
|
14509
|
+
},
|
|
14510
|
+
/**
|
|
14511
|
+
* The same discovery note for a planner, which also needs to know that its
|
|
14512
|
+
* commands go through `run_command`: Codex's own shell is sandboxed without
|
|
14513
|
+
* network, so `gh` or a cloud CLI fails there, and a model that tried its
|
|
14514
|
+
* shell first concludes the command cannot run.
|
|
14515
|
+
*/
|
|
14516
|
+
plannerInstructions() {
|
|
14517
|
+
const tools = ordewellToolNames("planner");
|
|
14518
|
+
return [
|
|
14519
|
+
`You have ${COUNT_WORDS[tools.length] ?? tools.length} tools from the \`${ORDEWELL_MCP_SERVER_NAME}\` MCP server: ${quoted2(tools)}.`,
|
|
14520
|
+
`They are not in your tool list up front. Find them with the tool discovery you have (the \`exec\` tool's \`ALL_TOOLS\` list) and call them by their full names, ${quoted2(this.toolNames("planner"))}.`,
|
|
14521
|
+
`Run shell commands with \`${this.toolName("run_command")}\`, not your own shell: yours is sandboxed without network, so git remotes, gh and cloud CLIs fail there.`
|
|
14522
|
+
].join(" ");
|
|
14090
14523
|
}
|
|
14091
14524
|
};
|
|
14092
14525
|
|
|
@@ -14274,6 +14707,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
14274
14707
|
interruptedTurnIds = /* @__PURE__ */ new Set();
|
|
14275
14708
|
/** A task's approval requests still waiting for an answer, by the request id as a string. */
|
|
14276
14709
|
openPermissions = /* @__PURE__ */ new Map();
|
|
14710
|
+
/** The tool each MCP server's latest call is for, which names the tool an approval asks about when its question does not. */
|
|
14711
|
+
mcpCallsInProgress = /* @__PURE__ */ new Map();
|
|
14277
14712
|
/**
|
|
14278
14713
|
* The files each in-flight `fileChange` item touches. Its approval request
|
|
14279
14714
|
* names only the item, and a person deciding needs to see the files.
|
|
@@ -14381,14 +14816,18 @@ ${this.exitMessage()}`);
|
|
|
14381
14816
|
return {
|
|
14382
14817
|
...common,
|
|
14383
14818
|
sandbox: "read-only",
|
|
14384
|
-
|
|
14819
|
+
// `on-request`, so a user's MCP tool reaches the planner's envelope as an approval
|
|
14820
|
+
// (ADR-0026); under `never` Codex fails the call without asking. Commands and file
|
|
14821
|
+
// changes it asks about are still declined: the sandbox holds, and commands go
|
|
14822
|
+
// through Ordewell's `run_command`.
|
|
14823
|
+
approvalPolicy: "on-request",
|
|
14385
14824
|
...threadConfig,
|
|
14386
14825
|
// `developerInstructions` layers on top of Codex's own base prompt, the
|
|
14387
14826
|
// way Claude Code's `--append-system-prompt` does. `baseInstructions`
|
|
14388
14827
|
// replaces it — which takes Codex's description of its own tools with
|
|
14389
14828
|
// it, and a planner that has forgotten it can read the workspace
|
|
14390
14829
|
// answers from a web search instead.
|
|
14391
|
-
developerInstructions: opts?.systemPrompt
|
|
14830
|
+
developerInstructions: [opts?.systemPrompt, opts?.mcp ? CODEX_ORDEWELL.plannerInstructions() : ""].filter(Boolean).join("\n\n") || void 0
|
|
14392
14831
|
};
|
|
14393
14832
|
}
|
|
14394
14833
|
/**
|
|
@@ -14492,7 +14931,7 @@ ${this.exitMessage()}`);
|
|
|
14492
14931
|
if (!open || !this.process) return false;
|
|
14493
14932
|
this.openPermissions.delete(id);
|
|
14494
14933
|
this.rpc.respond(open.requestId, TASK_APPROVALS[open.method].answer(decision.decision, open.params));
|
|
14495
|
-
const note = decision.decision === "deny" ? decision.note?.trim() : void 0;
|
|
14934
|
+
const note = decision.decision === "deny" && this.role === "task" ? decision.note?.trim() : void 0;
|
|
14496
14935
|
if (note && this.turn?.id) this.requestSteer(this.turn.id, note);
|
|
14497
14936
|
return true;
|
|
14498
14937
|
}
|
|
@@ -14778,10 +15217,12 @@ ${this.exitMessage()}`);
|
|
|
14778
15217
|
*/
|
|
14779
15218
|
answerServerRequest(msg, emit) {
|
|
14780
15219
|
const method = msg.method;
|
|
14781
|
-
|
|
15220
|
+
const params = msg.params ?? {};
|
|
15221
|
+
if (CODEX_ORDEWELL.isOrdewellAsk({ method, params })) {
|
|
14782
15222
|
this.rpc.respond(msg.id, { action: "accept", content: {} });
|
|
14783
15223
|
return;
|
|
14784
15224
|
}
|
|
15225
|
+
if (this.holdMcpToolApproval(msg, method, params, emit)) return;
|
|
14785
15226
|
const result = DECLINE_RESULTS[method];
|
|
14786
15227
|
if (result) {
|
|
14787
15228
|
this.rpc.respond(msg.id, result);
|
|
@@ -14793,9 +15234,30 @@ ${this.exitMessage()}`);
|
|
|
14793
15234
|
type: "permission_request",
|
|
14794
15235
|
id: String(msg.id ?? ""),
|
|
14795
15236
|
name: method.split("/").pop() ?? method,
|
|
14796
|
-
detail: JSON.stringify(msg.params ?? {})
|
|
15237
|
+
detail: JSON.stringify(msg.params ?? {}),
|
|
15238
|
+
decided: { decision: "deny" }
|
|
14797
15239
|
});
|
|
14798
15240
|
}
|
|
15241
|
+
/**
|
|
15242
|
+
* A planner's approval for a tool of the user's own MCP server, held for the
|
|
15243
|
+
* planner's envelope to answer (ADR-0026). Codex names the server; the tool
|
|
15244
|
+
* is in its question, or else the call it last started on that server.
|
|
15245
|
+
*/
|
|
15246
|
+
holdMcpToolApproval(msg, method, params, emit) {
|
|
15247
|
+
if (method !== "mcpServer/elicitation/request") return false;
|
|
15248
|
+
const meta = params._meta;
|
|
15249
|
+
const server = params.serverName;
|
|
15250
|
+
if (meta?.codex_approval_kind !== "mcp_tool_call" || typeof server !== "string") return false;
|
|
15251
|
+
const asked = typeof params.message === "string" ? /run tool "([^"]+)"/.exec(params.message)?.[1] : void 0;
|
|
15252
|
+
const tool2 = asked ?? this.mcpCallsInProgress.get(server);
|
|
15253
|
+
if (!tool2) return false;
|
|
15254
|
+
const id = String(msg.id);
|
|
15255
|
+
const input = typeof meta.tool_params === "object" && meta.tool_params !== null ? meta.tool_params : {};
|
|
15256
|
+
const scope = `mcp__${server}__${tool2}`;
|
|
15257
|
+
this.openPermissions.set(id, { requestId: msg.id, method, params });
|
|
15258
|
+
emit({ type: "permission_request", id, name: scope, detail: JSON.stringify(input), input, ask: { kind: "mcp", scope, tool: tool2, server } });
|
|
15259
|
+
return true;
|
|
15260
|
+
}
|
|
14799
15261
|
/** A tool item entering `inProgress` — announce the call so the timeline moves. */
|
|
14800
15262
|
emitItemStart(item, emit, subagentId) {
|
|
14801
15263
|
if (!item?.id) return;
|
|
@@ -14807,6 +15269,7 @@ ${this.exitMessage()}`);
|
|
|
14807
15269
|
emit({ type: "tool_call", id: item.id, name: "shell", args: { command: item.command, cwd: item.cwd }, ...subagentId ? { subagentId } : {} });
|
|
14808
15270
|
return;
|
|
14809
15271
|
case "mcpToolCall":
|
|
15272
|
+
if (item.server && item.tool) this.mcpCallsInProgress.set(item.server, item.tool);
|
|
14810
15273
|
emit({ type: "tool_call", id: item.id, name: shownToolName(item), args: item.arguments ?? {}, ...subagentId ? { subagentId } : {} });
|
|
14811
15274
|
return;
|
|
14812
15275
|
case "dynamicToolCall":
|
|
@@ -14979,6 +15442,55 @@ import { randomBytes as randomBytes6 } from "crypto";
|
|
|
14979
15442
|
// src/services/harness/openCodeTransport.ts
|
|
14980
15443
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
14981
15444
|
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
15445
|
+
var OPENCODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
15446
|
+
"bash",
|
|
15447
|
+
"edit",
|
|
15448
|
+
"write",
|
|
15449
|
+
"patch",
|
|
15450
|
+
"apply_patch",
|
|
15451
|
+
"multiedit",
|
|
15452
|
+
"read",
|
|
15453
|
+
"glob",
|
|
15454
|
+
"grep",
|
|
15455
|
+
"list",
|
|
15456
|
+
"lsp",
|
|
15457
|
+
"task",
|
|
15458
|
+
"question",
|
|
15459
|
+
"todowrite",
|
|
15460
|
+
"todoread",
|
|
15461
|
+
"skill",
|
|
15462
|
+
"websearch",
|
|
15463
|
+
"codesearch",
|
|
15464
|
+
"webfetch",
|
|
15465
|
+
"external_directory",
|
|
15466
|
+
"doom_loop",
|
|
15467
|
+
"invalid",
|
|
15468
|
+
"batch",
|
|
15469
|
+
"shell",
|
|
15470
|
+
"subagent",
|
|
15471
|
+
"browser",
|
|
15472
|
+
"execute",
|
|
15473
|
+
"search",
|
|
15474
|
+
"opencode_read_mcp_resource",
|
|
15475
|
+
"opencode_list_mcp_resources"
|
|
15476
|
+
]);
|
|
15477
|
+
function openCodePlannerAsk(permission, details, servers) {
|
|
15478
|
+
const meta = details.metadata ?? {};
|
|
15479
|
+
const pattern = details.patterns?.[0];
|
|
15480
|
+
if (permission === "webfetch") {
|
|
15481
|
+
const url = typeof meta.url === "string" ? meta.url : pattern;
|
|
15482
|
+
return url ? { kind: "fetch", url } : { kind: "other" };
|
|
15483
|
+
}
|
|
15484
|
+
if (permission === "external_directory") {
|
|
15485
|
+
if (typeof meta.filepath === "string") return { kind: "path", path: meta.filepath, directory: false };
|
|
15486
|
+
const dir = typeof meta.parentDir === "string" ? meta.parentDir : pattern?.replace(/[\\/]\*$/, "");
|
|
15487
|
+
return dir ? { kind: "path", path: dir, directory: true } : { kind: "other" };
|
|
15488
|
+
}
|
|
15489
|
+
if (OPENCODE_BUILTINS.has(permission)) return { kind: "other" };
|
|
15490
|
+
const server = servers.filter((s) => permission.startsWith(`${s}_`)).sort((a, b) => b.length - a.length)[0];
|
|
15491
|
+
if (!server) return { kind: "other" };
|
|
15492
|
+
return { kind: "mcp", scope: permission, tool: permission.slice(server.length + 1), server };
|
|
15493
|
+
}
|
|
14982
15494
|
function splitModelId(id) {
|
|
14983
15495
|
const slash = id.indexOf("/");
|
|
14984
15496
|
if (slash <= 0 || slash === id.length - 1) return null;
|
|
@@ -15072,7 +15584,6 @@ var PendingSteers = class {
|
|
|
15072
15584
|
}
|
|
15073
15585
|
};
|
|
15074
15586
|
var ALLOW = { decision: "allow" };
|
|
15075
|
-
var DENY = { decision: "deny" };
|
|
15076
15587
|
var OpenCodePermissions = class {
|
|
15077
15588
|
constructor(reply2, ordewell) {
|
|
15078
15589
|
this.reply = reply2;
|
|
@@ -15083,23 +15594,8 @@ var OpenCodePermissions = class {
|
|
|
15083
15594
|
/** Requests waiting for an answer, by request id, with the session that asked. */
|
|
15084
15595
|
open = /* @__PURE__ */ new Map();
|
|
15085
15596
|
/**
|
|
15086
|
-
* A
|
|
15087
|
-
*
|
|
15088
|
-
* announced so the timeline shows the planner reaching for something it may
|
|
15089
|
-
* not have. The planner's own submission path, which the allow rule should
|
|
15090
|
-
* already have settled, is the one request it may not be refused.
|
|
15091
|
-
*/
|
|
15092
|
-
refuse(request, sessionId, seen, onEvent) {
|
|
15093
|
-
if (!firstSight(request.id, seen)) return;
|
|
15094
|
-
if (this.isOrdewellTool(request.name)) {
|
|
15095
|
-
this.send(request.id, sessionId, ALLOW);
|
|
15096
|
-
return;
|
|
15097
|
-
}
|
|
15098
|
-
onEvent(request);
|
|
15099
|
-
this.send(request.id, sessionId, DENY);
|
|
15100
|
-
}
|
|
15101
|
-
/**
|
|
15102
|
-
* A task's request. Under a mode whose manifest sets `approvals: auto` it is
|
|
15597
|
+
* A task's request, or a planner's (with `autoApprove` false) for the
|
|
15598
|
+
* planner's envelope to answer (ADR-0026). Under a mode whose manifest sets `approvals: auto` it is
|
|
15103
15599
|
* answered at once with what `opencode run --auto` answers, so the same plan
|
|
15104
15600
|
* follows the manifest (ADR-0001), and announced already
|
|
15105
15601
|
* decided so the log still shows it. Any other mode leaves it open for an
|
|
@@ -15278,10 +15774,7 @@ async function streamEvents(connect, signal, onFrame, onConnected, onActivity) {
|
|
|
15278
15774
|
// src/services/harness/OpenCodeV2.ts
|
|
15279
15775
|
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
15280
15776
|
var IDLE_POLLS_BEFORE_START = 3;
|
|
15281
|
-
var PLANNER_RULES = [
|
|
15282
|
-
{ action: "question", resource: "*", effect: "deny" },
|
|
15283
|
-
{ action: "edit", resource: "*", effect: "deny" }
|
|
15284
|
-
];
|
|
15777
|
+
var PLANNER_RULES = Object.entries(OPENCODE_PLANNER_PERMISSION).map(([action, effect]) => ({ action, resource: "*", effect }));
|
|
15285
15778
|
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
15286
15779
|
function newTurnState() {
|
|
15287
15780
|
return { ...streamTurn(), toolNames: /* @__PURE__ */ new Map(), stepModels: /* @__PURE__ */ new Map(), briefs: /* @__PURE__ */ new Map(), streamedMessages: /* @__PURE__ */ new Set() };
|
|
@@ -15328,6 +15821,7 @@ var OpenCodeV2 = class {
|
|
|
15328
15821
|
return this.sessionId;
|
|
15329
15822
|
}
|
|
15330
15823
|
async start() {
|
|
15824
|
+
await this.exposeOrdewellTools();
|
|
15331
15825
|
const resume = this.opts.resumeSessionId;
|
|
15332
15826
|
if (resume) {
|
|
15333
15827
|
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
@@ -15349,6 +15843,19 @@ var OpenCodeV2 = class {
|
|
|
15349
15843
|
this.sessionId = created.data.id;
|
|
15350
15844
|
await this.applyInstructions();
|
|
15351
15845
|
}
|
|
15846
|
+
/**
|
|
15847
|
+
* 2.x lists an MCP server's tools inside its code-mode `execute` tool, where
|
|
15848
|
+
* a model told to call `task_complete` does not look for it. The server is
|
|
15849
|
+
* replaced at runtime with `codemode: false`, which lists its tools as tools
|
|
15850
|
+
* of their own: the config it started with is in the 1.x shape both versions
|
|
15851
|
+
* read, and that shape drops the setting. If the route is gone, the tools
|
|
15852
|
+
* are still reachable through `execute`.
|
|
15853
|
+
*/
|
|
15854
|
+
async exposeOrdewellTools() {
|
|
15855
|
+
const mcp = this.opts.mcp;
|
|
15856
|
+
if (!mcp) return;
|
|
15857
|
+
await this.host.request("PUT", `/api/experimental/mcp/${mcp.name}`, { config: { type: "remote", url: mcp.url, headers: mcp.headers, codemode: false } }).catch(() => void 0);
|
|
15858
|
+
}
|
|
15352
15859
|
/** Allow the Ordewell server's tools, so a call never waits on a person (ADR-0022, S3). */
|
|
15353
15860
|
ordewellRules() {
|
|
15354
15861
|
return this.opts.mcp ? OPENCODE_ORDEWELL.sessionRules() : [];
|
|
@@ -15609,12 +16116,14 @@ var OpenCodeV2 = class {
|
|
|
15609
16116
|
...ask.source?.id ? { toolUseId: ask.source.id } : {}
|
|
15610
16117
|
};
|
|
15611
16118
|
}
|
|
15612
|
-
/** A planner's request is
|
|
16119
|
+
/** A planner's request is held for its envelope (ADR-0026); a task's is answered at once or left open for a card. */
|
|
15613
16120
|
onPermissionAsked(ask, session, state, onEvent) {
|
|
15614
16121
|
const request = this.permissionRequest(ask);
|
|
15615
16122
|
if (!request) return;
|
|
15616
|
-
if (this.role !== "task")
|
|
15617
|
-
|
|
16123
|
+
if (this.role !== "task") {
|
|
16124
|
+
const target = openCodePlannerAsk(request.name, { patterns: ask.resources }, this.host.mcpServers?.() ?? []);
|
|
16125
|
+
this.permissions.ask({ ...request, ask: target }, session, false, state.seen, onEvent);
|
|
16126
|
+
} else this.permissions.ask(request, session, this.opts.kind === "task" && autoApproves(this.opts), state.seen, onEvent);
|
|
15618
16127
|
}
|
|
15619
16128
|
};
|
|
15620
16129
|
|
|
@@ -15632,7 +16141,9 @@ var DISABLED_TOOLS = {
|
|
|
15632
16141
|
edit: false,
|
|
15633
16142
|
write: false,
|
|
15634
16143
|
apply_patch: false,
|
|
15635
|
-
todowrite: false
|
|
16144
|
+
todowrite: false,
|
|
16145
|
+
bash: false,
|
|
16146
|
+
task: false
|
|
15636
16147
|
};
|
|
15637
16148
|
var TASK_DISABLED_TOOLS = { question: false };
|
|
15638
16149
|
function describeError(err) {
|
|
@@ -15703,6 +16214,8 @@ var OpenCodeAdapter = class {
|
|
|
15703
16214
|
taskTurn = null;
|
|
15704
16215
|
/** An abort was posted during the current task turn, so the idle that follows ends it as interrupted. */
|
|
15705
16216
|
interruptRequested = false;
|
|
16217
|
+
/** The user's MCP servers, as `/mcp` last listed them: what tells a planner's MCP tool request from a built-in's (ADR-0026). */
|
|
16218
|
+
mcpServers = [];
|
|
15706
16219
|
permissions = new OpenCodePermissions(
|
|
15707
16220
|
(id, sessionId, decision) => this.replyPermission(id, sessionId, permissionReply(decision, "reply")),
|
|
15708
16221
|
() => this.ordewell
|
|
@@ -15735,7 +16248,8 @@ var OpenCodeAdapter = class {
|
|
|
15735
16248
|
command: "opencode",
|
|
15736
16249
|
args: ["serve", "--hostname", "127.0.0.1", "--port", "0"],
|
|
15737
16250
|
env: (workspaceEnv) => {
|
|
15738
|
-
const
|
|
16251
|
+
const existing = workspaceEnv[OPENCODE_CONFIG_VARIABLE] ?? process.env[OPENCODE_CONFIG_VARIABLE];
|
|
16252
|
+
const ordewellConfig = opts.kind === "planner" ? OPENCODE_ORDEWELL.configContent(existing, opts.mcp ?? null, "planner") ?? OPENCODE_ORDEWELL.configContent(void 0, opts.mcp ?? null, "planner") : opts.mcp ? OPENCODE_ORDEWELL.configContent(existing, opts.mcp, "task") : null;
|
|
15739
16253
|
if (opts.mcp && ordewellConfig === null) {
|
|
15740
16254
|
console.error("[opencode] OPENCODE_CONFIG_CONTENT is not a JSON object, so the Ordewell tools were not injected.");
|
|
15741
16255
|
}
|
|
@@ -15793,7 +16307,8 @@ var OpenCodeAdapter = class {
|
|
|
15793
16307
|
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
15794
16308
|
processEnded: this.processEnded,
|
|
15795
16309
|
isExited: () => this.exited,
|
|
15796
|
-
exitMessage: () => this.exitMessage()
|
|
16310
|
+
exitMessage: () => this.exitMessage(),
|
|
16311
|
+
mcpServers: () => this.mcpServers
|
|
15797
16312
|
}, { ...opts, mcp: this.ordewell ?? void 0 }, this.role());
|
|
15798
16313
|
await this.v2.start();
|
|
15799
16314
|
this.sessionId = this.v2.nativeSessionId();
|
|
@@ -16259,7 +16774,15 @@ var OpenCodeAdapter = class {
|
|
|
16259
16774
|
const sessionId = ask.sessionID ?? this.sessionId ?? "";
|
|
16260
16775
|
const name = permissionName(ask);
|
|
16261
16776
|
if (!this.task) {
|
|
16262
|
-
this.permissions.
|
|
16777
|
+
this.permissions.ask({
|
|
16778
|
+
type: "permission_request",
|
|
16779
|
+
id,
|
|
16780
|
+
name,
|
|
16781
|
+
detail: permissionDetail(ask),
|
|
16782
|
+
input: ask.metadata ?? {},
|
|
16783
|
+
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {},
|
|
16784
|
+
ask: openCodePlannerAsk(name, ask, this.mcpServers)
|
|
16785
|
+
}, sessionId, false, seen, onEvent);
|
|
16263
16786
|
return;
|
|
16264
16787
|
}
|
|
16265
16788
|
const request = {
|
|
@@ -16316,10 +16839,21 @@ var OpenCodeAdapter = class {
|
|
|
16316
16839
|
if (!ordewell || !this.baseUrl) return false;
|
|
16317
16840
|
return awaitAttach(async () => {
|
|
16318
16841
|
if (this.exited) return "failed";
|
|
16319
|
-
const servers = await this.json("GET", "/mcp").catch(() => null);
|
|
16842
|
+
const servers = this.v2 ? await this.v2McpStatus() : await this.json("GET", "/mcp").catch(() => null);
|
|
16843
|
+
if (servers) this.mcpServers = Object.keys(servers).filter((name) => name !== ordewell.name);
|
|
16320
16844
|
return OPENCODE_ORDEWELL.attachState(servers?.[ordewell.name]?.status);
|
|
16321
16845
|
}, MCP_ATTACH_TIMEOUT_MS3, MCP_STATUS_POLL_MS2);
|
|
16322
16846
|
}
|
|
16847
|
+
/** 2.x lists its servers as `{ name, status: { status } }`, and only once it has started connecting them. */
|
|
16848
|
+
async v2McpStatus() {
|
|
16849
|
+
const listed2 = await this.json("GET", "/api/mcp").catch(() => null);
|
|
16850
|
+
if (!Array.isArray(listed2?.data)) return null;
|
|
16851
|
+
const servers = {};
|
|
16852
|
+
for (const { name, status } of listed2.data) {
|
|
16853
|
+
if (typeof name === "string") servers[name] = typeof status?.status === "string" ? { status: status.status } : {};
|
|
16854
|
+
}
|
|
16855
|
+
return servers;
|
|
16856
|
+
}
|
|
16323
16857
|
dispose() {
|
|
16324
16858
|
if (this.disposed) return;
|
|
16325
16859
|
this.disposed = true;
|
|
@@ -18891,6 +19425,10 @@ function plannerToolHandler(host) {
|
|
|
18891
19425
|
}
|
|
18892
19426
|
return { text: skill.content };
|
|
18893
19427
|
},
|
|
19428
|
+
async runCommand({ command }, { signal }) {
|
|
19429
|
+
const outcome = await host.runCommand(command, signal);
|
|
19430
|
+
return { text: outcome.output || (outcome.success ? "(no output)" : "The command failed with no output."), ...outcome.success ? {} : { isError: true } };
|
|
19431
|
+
},
|
|
18894
19432
|
async listRunners() {
|
|
18895
19433
|
const catalog = await host.liveCatalog();
|
|
18896
19434
|
return answer({
|
|
@@ -19140,6 +19678,47 @@ var TurnStream = class {
|
|
|
19140
19678
|
}
|
|
19141
19679
|
};
|
|
19142
19680
|
|
|
19681
|
+
// src/services/harness/plannerPermissions.ts
|
|
19682
|
+
var READ_ONLY_DENIAL = "The Ordewell planner is read-only: it changes no files. Use run_command for commands; changes belong to the plan's tasks.";
|
|
19683
|
+
var OPS_HINT = "Continue without it. If it would change something outside the repository, add it to the plan as an ops task; if it only reads, ask the user to approve it or add it to the planner allowlist.";
|
|
19684
|
+
var ALLOW2 = { decision: "allow" };
|
|
19685
|
+
var SUBJECT_MAX_CHARS = 400;
|
|
19686
|
+
function deny(note) {
|
|
19687
|
+
return { decision: "deny", note };
|
|
19688
|
+
}
|
|
19689
|
+
async function decidePlannerPermission(ask, input, gate) {
|
|
19690
|
+
if (!ask || ask.kind === "other") return deny(READ_ONLY_DENIAL);
|
|
19691
|
+
if (!gate) return deny("Nobody can approve this here. Continue without it.");
|
|
19692
|
+
switch (ask.kind) {
|
|
19693
|
+
case "mcp": {
|
|
19694
|
+
const args = JSON.stringify(input);
|
|
19695
|
+
const subject = `${ask.server ?? "MCP"} \xB7 ${ask.tool} ${args.length > SUBJECT_MAX_CHARS ? `${args.slice(0, SUBJECT_MAX_CHARS)}\u2026` : args}`;
|
|
19696
|
+
const granted = await gate.approval.request({
|
|
19697
|
+
kind: "mcp_tool",
|
|
19698
|
+
subject,
|
|
19699
|
+
scope: ask.scope,
|
|
19700
|
+
tool: ask.tool,
|
|
19701
|
+
detail: `The planner wants to use "${ask.tool}"${ask.server ? ` from ${ask.server}` : ""}. It may change things outside the repository.`
|
|
19702
|
+
});
|
|
19703
|
+
return granted ? ALLOW2 : deny(`Not approved: ${ask.scope}. ${OPS_HINT}`);
|
|
19704
|
+
}
|
|
19705
|
+
case "fetch":
|
|
19706
|
+
if (!gate.fetcher) return deny("This fetch cannot be approved here.");
|
|
19707
|
+
return await gate.fetcher.confirm(ask.url) ? ALLOW2 : deny(`Fetch denied (blocked host or not approved): ${ask.url}. ${OPS_HINT}`);
|
|
19708
|
+
case "path": {
|
|
19709
|
+
const { abs, inside } = resolveWithin(gate.workspaceRoot, ask.path);
|
|
19710
|
+
if (inside) return ALLOW2;
|
|
19711
|
+
const granted = await gate.approval.request({
|
|
19712
|
+
kind: "external_path",
|
|
19713
|
+
subject: abs,
|
|
19714
|
+
scope: grantScopeFor(abs, ask.directory ? "directory" : "file"),
|
|
19715
|
+
detail: `Planner research wants to read ${abs}, outside the workspace (${gate.workspaceRoot}).`
|
|
19716
|
+
});
|
|
19717
|
+
return granted ? ALLOW2 : deny(`Access denied: "${abs}" is outside the workspace and was not approved.`);
|
|
19718
|
+
}
|
|
19719
|
+
}
|
|
19720
|
+
}
|
|
19721
|
+
|
|
19143
19722
|
// src/services/harness/CliAgentAiService.ts
|
|
19144
19723
|
var MAX_AGENT_WAITS = 2;
|
|
19145
19724
|
function waitForAgentsPrompt(running) {
|
|
@@ -19183,6 +19762,7 @@ var CliAgentAiService = class {
|
|
|
19183
19762
|
* every session boundary — nothing from one goal may reach the next.
|
|
19184
19763
|
*/
|
|
19185
19764
|
lastNativeSessionId = null;
|
|
19765
|
+
/** `gate` is what the agent's own tool requests are decided against; absent, only Ordewell's tools are allowed (ADR-0026). */
|
|
19186
19766
|
conversation = null;
|
|
19187
19767
|
mcpServer;
|
|
19188
19768
|
/** The planner token the running process was spawned with; revoked with the process (ADR-0022, A3). */
|
|
@@ -19251,7 +19831,8 @@ var CliAgentAiService = class {
|
|
|
19251
19831
|
};
|
|
19252
19832
|
const tools = req.plannerTools;
|
|
19253
19833
|
await this.startAdapter(startOptions, tools);
|
|
19254
|
-
|
|
19834
|
+
const gate = req.approval ? { approval: req.approval, fetcher: req.fetcher, workspaceRoot: startOptions.cwd } : void 0;
|
|
19835
|
+
this.conversation = { startOptions, tools, gate };
|
|
19255
19836
|
const opening = this.openingMessage(req);
|
|
19256
19837
|
return this.runConversation(opening, req.onProgress, req.signal);
|
|
19257
19838
|
}
|
|
@@ -19341,6 +19922,8 @@ var CliAgentAiService = class {
|
|
|
19341
19922
|
segmentId = uuidv44();
|
|
19342
19923
|
};
|
|
19343
19924
|
const streamedThinking = /* @__PURE__ */ new Set();
|
|
19925
|
+
const deniedCalls = /* @__PURE__ */ new Set();
|
|
19926
|
+
const gate = this.conversation?.gate;
|
|
19344
19927
|
const subagentUsage = /* @__PURE__ */ new Map();
|
|
19345
19928
|
let error;
|
|
19346
19929
|
let stepIndex = 0;
|
|
@@ -19395,7 +19978,7 @@ var CliAgentAiService = class {
|
|
|
19395
19978
|
return;
|
|
19396
19979
|
}
|
|
19397
19980
|
case "tool_result":
|
|
19398
|
-
settle(event.id, event.output, event.success, event.success ? "success" : "failure", event.subagentId);
|
|
19981
|
+
settle(event.id, event.output, event.success, event.success ? "success" : deniedCalls.has(event.id) ? "denied" : "failure", event.subagentId);
|
|
19399
19982
|
return;
|
|
19400
19983
|
case "usage": {
|
|
19401
19984
|
const { subagentId } = event.record;
|
|
@@ -19423,18 +20006,28 @@ var CliAgentAiService = class {
|
|
|
19423
20006
|
backgroundAgents++;
|
|
19424
20007
|
return;
|
|
19425
20008
|
case "permission_request": {
|
|
19426
|
-
const
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
pendingCalls.
|
|
19430
|
-
|
|
20009
|
+
const announceDenial = (output) => {
|
|
20010
|
+
const mapped = mapAgentTool(event.name);
|
|
20011
|
+
const args = JSON.stringify({ detail: event.detail });
|
|
20012
|
+
if (!pendingCalls.has(event.id)) {
|
|
20013
|
+
pendingCalls.set(event.id, { tool: mapped.tool, toolLabel: mapped.toolLabel, args });
|
|
20014
|
+
onProgress({ type: "tool_call", tool: mapped.tool, toolLabel: mapped.toolLabel, toolArgs: args, toolCallId: event.id });
|
|
20015
|
+
}
|
|
20016
|
+
settle(event.id, output, false, "denied");
|
|
20017
|
+
};
|
|
20018
|
+
if (!event.decided && adapter.answerPermission) {
|
|
20019
|
+
const { id, toolUseId } = event;
|
|
20020
|
+
void decidePlannerPermission(event.ask, event.input ?? {}, gate).catch(() => ({ decision: "deny" })).then((decision) => {
|
|
20021
|
+
if (decision.decision === "deny") {
|
|
20022
|
+
if (toolUseId) deniedCalls.add(toolUseId);
|
|
20023
|
+
else announceDenial(`Access denied: "${event.name}" was refused. ${decision.note ?? ""}`.trim());
|
|
20024
|
+
}
|
|
20025
|
+
adapter.answerPermission?.(id, decision);
|
|
20026
|
+
});
|
|
20027
|
+
return;
|
|
19431
20028
|
}
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
`Access denied: the planner runs read-only, so "${event.name}" was refused. Mutation belongs to the runners that execute the plan.`,
|
|
19435
|
-
false,
|
|
19436
|
-
"denied"
|
|
19437
|
-
);
|
|
20029
|
+
if (event.decided?.decision !== "deny") return;
|
|
20030
|
+
announceDenial(`Access denied: the planner runs read-only, so "${event.name}" was refused. Mutation belongs to the runners that execute the plan.`);
|
|
19438
20031
|
return;
|
|
19439
20032
|
}
|
|
19440
20033
|
case "error":
|
|
@@ -20804,7 +21397,7 @@ var HttpWebFetcher = class {
|
|
|
20804
21397
|
}
|
|
20805
21398
|
async fetch(url) {
|
|
20806
21399
|
if (!await this.confirm(url)) {
|
|
20807
|
-
return { success: false, output: `Fetch denied (blocked host or not approved): ${url}
|
|
21400
|
+
return { success: false, output: `Fetch denied (blocked host or not approved): ${url}. Continue without it, or ask the user to approve this origin or add it to the planner allowlist.`, truncated: false };
|
|
20808
21401
|
}
|
|
20809
21402
|
return this.get(url, "Fetch");
|
|
20810
21403
|
}
|
|
@@ -21818,8 +22411,9 @@ function createSession(deps) {
|
|
|
21818
22411
|
checkpointQuestion: (taskId) => orchestrator.getCheckpointQuestion(taskId)
|
|
21819
22412
|
});
|
|
21820
22413
|
const approvalPolicy = new ApprovalPolicy({
|
|
21821
|
-
|
|
21822
|
-
|
|
22414
|
+
// Read per request, so a change of autonomy level or allowlist applies to the next call.
|
|
22415
|
+
mode: () => effectiveApprovalMode(deps.config.approvalMode, deps.config.autonomousMode),
|
|
22416
|
+
preApproved: () => [...deps.config.approvalDefaults ? DEFAULT_PLANNER_ALLOWLIST : [], ...deps.config.approvalPreApproved],
|
|
21823
22417
|
ask: (req) => approvals.ask(req),
|
|
21824
22418
|
// The interactive path (`asked`) already broadcasts approval_request +
|
|
21825
22419
|
// approval_settled; only the silent sources need a signal, or a
|
|
@@ -21922,7 +22516,8 @@ var Session = class {
|
|
|
21922
22516
|
read: (signature, answer2) => this.conversation.read(signature, answer2),
|
|
21923
22517
|
liveOutput: (taskId, opts) => this.orchestrator.getLiveOutput(taskId, opts),
|
|
21924
22518
|
lastAttempt: (taskId) => lastAttemptDigest(this.taskLogLocation, taskId),
|
|
21925
|
-
taskSkills: () => this.workspaceSkills()
|
|
22519
|
+
taskSkills: () => this.workspaceSkills(),
|
|
22520
|
+
runCommand: (command, signal) => this.fsAdapter.bash(command, signal)
|
|
21926
22521
|
});
|
|
21927
22522
|
constructor(parts) {
|
|
21928
22523
|
this.config = parts.config;
|
|
@@ -22379,6 +22974,7 @@ var Session = class {
|
|
|
22379
22974
|
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
22380
22975
|
fs: this.fsAdapter,
|
|
22381
22976
|
fetcher: this.fetcher,
|
|
22977
|
+
approval: this.approvalPolicy,
|
|
22382
22978
|
plannerTools: { sessionId: this.sessionId, handler: this.plannerTools },
|
|
22383
22979
|
skills: this.workspaceSkills().listSkills()
|
|
22384
22980
|
};
|
|
@@ -24009,6 +24605,7 @@ export {
|
|
|
24009
24605
|
DAEMON_SUBPROTOCOL,
|
|
24010
24606
|
DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
|
|
24011
24607
|
DEFAULT_MAX_PARALLEL,
|
|
24608
|
+
DEFAULT_PLANNER_ALLOWLIST,
|
|
24012
24609
|
DEFAULT_RUNNERS,
|
|
24013
24610
|
DENY_ALL,
|
|
24014
24611
|
EMPTY_CONVERSATION,
|
|
@@ -24145,6 +24742,7 @@ export {
|
|
|
24145
24742
|
coerceAssignments,
|
|
24146
24743
|
collectDirectDependencyOutputs,
|
|
24147
24744
|
collectProviderCredentials,
|
|
24745
|
+
commandAllowedBy,
|
|
24148
24746
|
composeAugmentedPrompt,
|
|
24149
24747
|
composeContinuationPrompt,
|
|
24150
24748
|
configuredProviders,
|
|
@@ -24171,6 +24769,7 @@ export {
|
|
|
24171
24769
|
discoverGeminiModels,
|
|
24172
24770
|
drainNext,
|
|
24173
24771
|
effectiveAllowlist,
|
|
24772
|
+
effectiveApprovalMode,
|
|
24174
24773
|
emptyWarnings,
|
|
24175
24774
|
enabledRunners,
|
|
24176
24775
|
ensureDir,
|
|
@@ -24253,6 +24852,8 @@ export {
|
|
|
24253
24852
|
outputLines,
|
|
24254
24853
|
outputPreview,
|
|
24255
24854
|
ownerOnlyConfigFile,
|
|
24855
|
+
parseAllowlist,
|
|
24856
|
+
parseApprovalModeSetting,
|
|
24256
24857
|
parseAutonomyLevel,
|
|
24257
24858
|
parseMaxParallel,
|
|
24258
24859
|
parsePartialPlan,
|
|
@@ -24310,6 +24911,7 @@ export {
|
|
|
24310
24911
|
savePrdMarkdown,
|
|
24311
24912
|
saveSession,
|
|
24312
24913
|
saveState,
|
|
24914
|
+
scopeAllowed,
|
|
24313
24915
|
serializePlan,
|
|
24314
24916
|
serializeTask,
|
|
24315
24917
|
serializeTaskStatus,
|
|
@@ -24347,6 +24949,7 @@ export {
|
|
|
24347
24949
|
toTaskLogEvent,
|
|
24348
24950
|
tokenSubprotocols,
|
|
24349
24951
|
tokensMatch,
|
|
24952
|
+
toolAllowed,
|
|
24350
24953
|
toolHeadline,
|
|
24351
24954
|
trimToolOutput,
|
|
24352
24955
|
truncateCheckpointSummary,
|