wayang-ai 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5516,22 +5516,22 @@ function fitWorkerRows(rows, budget) {
5516
5516
  const fitted = [];
5517
5517
  let used = 0;
5518
5518
  for (const row of rows) {
5519
- const sep2 = fitted.length > 0 ? stringWidth(ROW_SEPARATOR) : 0;
5519
+ const sep3 = fitted.length > 0 ? stringWidth(ROW_SEPARATOR) : 0;
5520
5520
  const full = workerRowText(row, true);
5521
5521
  const fullWidth = stringWidth(full);
5522
- if (used + sep2 + fullWidth <= budget) {
5522
+ if (used + sep3 + fullWidth <= budget) {
5523
5523
  fitted.push(full);
5524
- used += sep2 + fullWidth;
5524
+ used += sep3 + fullWidth;
5525
5525
  continue;
5526
5526
  }
5527
5527
  const compact = workerRowText(row, false);
5528
5528
  const compactWidth = stringWidth(compact);
5529
- if (used + sep2 + compactWidth <= budget) {
5529
+ if (used + sep3 + compactWidth <= budget) {
5530
5530
  fitted.push(compact);
5531
- used += sep2 + compactWidth;
5531
+ used += sep3 + compactWidth;
5532
5532
  continue;
5533
5533
  }
5534
- const remaining = budget - used - sep2;
5534
+ const remaining = budget - used - sep3;
5535
5535
  if (remaining > 0) {
5536
5536
  fitted.push(fitWithSuffix(row.label, rowSuffix(row), remaining));
5537
5537
  }
@@ -5938,7 +5938,7 @@ function AppContent() {
5938
5938
  // package.json
5939
5939
  var package_default = {
5940
5940
  name: "wayang-ai",
5941
- version: "0.9.5",
5941
+ version: "0.9.6",
5942
5942
  description: "Multi-agent orchestration platform",
5943
5943
  type: "module",
5944
5944
  license: "MIT",
@@ -9356,6 +9356,9 @@ function classifyRedirections(segments) {
9356
9356
  return result2;
9357
9357
  }
9358
9358
 
9359
+ // src/services/tools/permission-rules/bash-capability.ts
9360
+ import { resolve as resolvePath, sep as sep2 } from "path";
9361
+
9359
9362
  // src/services/tools/permission-rules/bash-head.ts
9360
9363
  var DANGEROUS_HEAD_COMMANDS = /* @__PURE__ */ new Set([
9361
9364
  "sudo",
@@ -9840,6 +9843,11 @@ function isWriteSegment(segment) {
9840
9843
  }
9841
9844
  return false;
9842
9845
  }
9846
+ function isWriteCommandHead(command, words) {
9847
+ if (BASH_WRITE_VERBS.has(command)) return true;
9848
+ const flags2 = BASH_FLAGGED_WRITE_VERBS.get(command);
9849
+ return flags2 !== void 0 && flags2.some((flag) => words.some((other) => isFlagWord(other, flag)));
9850
+ }
9843
9851
  var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
9844
9852
  "status",
9845
9853
  "log",
@@ -9879,6 +9887,10 @@ function isReadOnlyGitCommand(words) {
9879
9887
  const flags2 = args.slice(i + 1).filter((word) => word.startsWith("-"));
9880
9888
  return flags2.length > 0 && flags2.every((flag) => listFlags.includes(flag));
9881
9889
  }
9890
+ function isDateSetFlag(word) {
9891
+ if (word === "-s" || word.startsWith("--set")) return true;
9892
+ return /^-[^-]*s/.test(word);
9893
+ }
9882
9894
  function classifySegment(segment) {
9883
9895
  if (isWriteSegment(segment)) return { mode: "write" };
9884
9896
  const head = headCommand(segment);
@@ -9887,6 +9899,11 @@ function classifySegment(segment) {
9887
9899
  if (head.command === "git") {
9888
9900
  return { mode: isReadOnlyGitCommand(head.words) ? "read" : "unknown" };
9889
9901
  }
9902
+ if (head.command === "date") {
9903
+ const sets = head.words.slice(1).some(isDateSetFlag);
9904
+ if (sets) return { mode: "unknown", unverified: "date" };
9905
+ return { mode: "read" };
9906
+ }
9890
9907
  if (head.command === "sed") {
9891
9908
  const scripts = sedScriptsOf(head.words);
9892
9909
  if (scripts === void 0) return { mode: "unknown", unverified: "sed" };
@@ -9953,99 +9970,6 @@ function unanalyzedInnerCommand(segments) {
9953
9970
  return void 0;
9954
9971
  }
9955
9972
 
9956
- // src/services/tools/permission-rules/bash-paths.ts
9957
- function pathPieces(text, access) {
9958
- const candidates = [];
9959
- for (const piece of text.split("=")) {
9960
- if (!piece.startsWith("/") && !piece.startsWith("~")) continue;
9961
- if (isDiscardRedirectTarget(piece)) continue;
9962
- candidates.push({ path: piece, access });
9963
- }
9964
- return candidates;
9965
- }
9966
- function extractPathCandidates(segments) {
9967
- const candidates = [];
9968
- for (const segment of segments) {
9969
- const segmentLevel = segmentAccess(segment);
9970
- for (const token of segment.tokens) {
9971
- if (token.kind === "word") {
9972
- candidates.push(...pathPieces(token.word.text, segmentLevel));
9973
- continue;
9974
- }
9975
- const { redirect } = token;
9976
- if (redirect.kind === "fd-dup" || redirect.kind === "here-doc" || redirect.kind === "here-string") continue;
9977
- const target = redirect.target?.text;
9978
- if (target === void 0 || isDiscardRedirectTarget(target)) continue;
9979
- if (target.includes(CMDSUBST_PLACEHOLDER)) continue;
9980
- const access = redirect.kind === "output" ? "write" : segmentLevel;
9981
- for (const candidate of pathPieces(target, access)) {
9982
- candidates.push(
9983
- redirect.kind === "output" ? { ...candidate, reason: `output redirection to "${target}"` } : candidate
9984
- );
9985
- }
9986
- }
9987
- }
9988
- return candidates;
9989
- }
9990
- function commandAccess(candidates) {
9991
- let access = "unknown";
9992
- for (const candidate of candidates) {
9993
- if (candidate.access === "write") return "write";
9994
- if (candidate.access === "read") access = "read";
9995
- }
9996
- return access;
9997
- }
9998
-
9999
- // src/services/tools/permission-rules/cd-chain.ts
10000
- var NON_LITERAL_TARGET_CHARS = /[$`*?[\]{}]/;
10001
- function literalCdTarget(segment) {
10002
- if (segment.tokens.length !== 2) return void 0;
10003
- const [command, argument] = segment.tokens;
10004
- if (command.kind !== "word" || argument.kind !== "word") return void 0;
10005
- if (command.word.text !== "cd") return void 0;
10006
- if (command.word.substitutions.length > 0) return void 0;
10007
- const target = argument.word.text;
10008
- if (target === "" || target === "-") return void 0;
10009
- if (argument.word.substitutions.length > 0) return void 0;
10010
- if (target.includes(CMDSUBST_PLACEHOLDER)) return void 0;
10011
- if (NON_LITERAL_TARGET_CHARS.test(target)) return void 0;
10012
- return target;
10013
- }
10014
- function cdAskReason(scope, argument, abs, verdict) {
10015
- if (verdict.sensitive === true && verdict.matchedEntry !== void 0) {
10016
- return `cd target "${argument}" is the sensitive location ${verdict.matchedEntry} (${abs}) \u2014 controller approval is required`;
10017
- }
10018
- if (verdict.matchedRule === "read-within-home") {
10019
- return `cd target "${argument}" is inside the home directory (${abs}) \u2014 controller approval is required`;
10020
- }
10021
- return `cd target "${argument}" resolves outside the allowed scope (${abs}) \u2014 controller approval is required`;
10022
- }
10023
- function applyCdChain(scope, segments) {
10024
- let cwd = scope.workspace;
10025
- let i = 0;
10026
- while (i + 1 < segments.length && segments[i].operator === "&&") {
10027
- const argument = literalCdTarget(segments[i]);
10028
- if (argument === void 0) break;
10029
- const expanded = expandHome(argument);
10030
- if (expanded.unresolvable) break;
10031
- const abs = normalizePath(expanded.path, { base: cwd });
10032
- const verdict = decidePath(scope, abs, "read");
10033
- if (verdict.decision === "deny") {
10034
- return { segments: segments.slice(i), decision: toToolDecision(verdict, "read") };
10035
- }
10036
- const narrowed = narrowHomeRead(verdict, { path: abs, access: "read" });
10037
- if (narrowed.decision === "ask") {
10038
- return {
10039
- segments: segments.slice(i),
10040
- decision: { ...narrowed, reason: cdAskReason(scope, argument, abs, narrowed) }
10041
- };
10042
- }
10043
- cwd = abs;
10044
- i += 1;
10045
- }
10046
- return { segments: segments.slice(i) };
10047
- }
10048
-
10049
9973
  // src/services/tools/permission-rules/substitutions.ts
10050
9974
  var MAX_CMDSUBST_DEPTH = 5;
10051
9975
  function collectSubstitutions(segments) {
@@ -10223,6 +10147,355 @@ function unanalyzedScriptFile(segments) {
10223
10147
  return void 0;
10224
10148
  }
10225
10149
 
10150
+ // src/services/tools/permission-rules/bash-capability.ts
10151
+ var INTERPRETER_COMMANDS = /* @__PURE__ */ new Set([
10152
+ "bash",
10153
+ "sh",
10154
+ "dash",
10155
+ "zsh",
10156
+ "ksh",
10157
+ "busybox",
10158
+ "python",
10159
+ "python3",
10160
+ "node",
10161
+ "perl",
10162
+ "ruby",
10163
+ "awk",
10164
+ "php",
10165
+ "lua"
10166
+ ]);
10167
+ var NETWORK_COMMANDS = /* @__PURE__ */ new Set(["curl", "wget", "nc", "scp"]);
10168
+ var WRITE_CAPABILITY_COMMANDS = /* @__PURE__ */ new Set(["rsync"]);
10169
+ var INERT_COMMANDS = /* @__PURE__ */ new Set([
10170
+ "sleep",
10171
+ "true",
10172
+ "false",
10173
+ ":",
10174
+ "pwd",
10175
+ "whoami",
10176
+ "id",
10177
+ "date",
10178
+ "test",
10179
+ "[",
10180
+ "exit",
10181
+ "which",
10182
+ "command",
10183
+ "echo",
10184
+ "printf"
10185
+ ]);
10186
+ var LAUNCHER_COMMANDS = /* @__PURE__ */ new Set([
10187
+ "script",
10188
+ "su",
10189
+ "setsid",
10190
+ "watch",
10191
+ "timeout",
10192
+ "ssh"
10193
+ ]);
10194
+ var DYNAMIC_COMMANDS = /* @__PURE__ */ new Set(["source", "."]);
10195
+ var ASSIGNMENT_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*=/;
10196
+ var NUMERIC_PATTERN = /^\d+$/;
10197
+ var DYNAMIC_WORD_PATTERN2 = /[$`]/;
10198
+ function wordTexts(segment) {
10199
+ const words = [];
10200
+ for (const token of segment.tokens) {
10201
+ if (token.kind === "word") words.push(token.word.text);
10202
+ }
10203
+ return words;
10204
+ }
10205
+ function stripAssignment(word) {
10206
+ const match = word.match(/^[A-Za-z_][A-Za-z0-9_]*=(.*)$/);
10207
+ return match === null ? word : match[1];
10208
+ }
10209
+ function relativeOperands(words) {
10210
+ const operands = [];
10211
+ for (const word of words.slice(1)) {
10212
+ if (word.startsWith("-")) continue;
10213
+ if (word.includes(CMDSUBST_PLACEHOLDER)) continue;
10214
+ const value = stripAssignment(word);
10215
+ if (value === "" || value.startsWith("/") || value.startsWith("~")) continue;
10216
+ operands.push(value);
10217
+ }
10218
+ return operands;
10219
+ }
10220
+ function hasOutputRedirect(segment) {
10221
+ for (const token of segment.tokens) {
10222
+ if (token.kind !== "redirect") continue;
10223
+ const { redirect } = token;
10224
+ if (redirect.kind !== "output") continue;
10225
+ if (isDiscardRedirectTarget(redirect.target?.text)) continue;
10226
+ return true;
10227
+ }
10228
+ return false;
10229
+ }
10230
+ function relativeInputTargets(segment) {
10231
+ const targets = [];
10232
+ for (const token of segment.tokens) {
10233
+ if (token.kind !== "redirect") continue;
10234
+ const { redirect } = token;
10235
+ if (redirect.kind !== "input") continue;
10236
+ const target = redirect.target?.text;
10237
+ if (target === void 0 || isDiscardRedirectTarget(target)) continue;
10238
+ if (target.includes(CMDSUBST_PLACEHOLDER)) continue;
10239
+ if (target.startsWith("/") || target.startsWith("~")) continue;
10240
+ targets.push(target);
10241
+ }
10242
+ return targets;
10243
+ }
10244
+ function insideWorkspace(scope, abs) {
10245
+ const workspace = resolvePath(scope.workspace);
10246
+ return abs === workspace || abs.startsWith(`${workspace}${sep2}`);
10247
+ }
10248
+ function capabilityFromWords(words) {
10249
+ let start = 0;
10250
+ while (start < words.length && ASSIGNMENT_PATTERN2.test(words[start])) start += 1;
10251
+ const command = words[start];
10252
+ if (command === void 0) return "unknown";
10253
+ return capabilityOfHead(command, words.slice(start));
10254
+ }
10255
+ function launcherCapability(command, words) {
10256
+ const args = words.slice(1);
10257
+ for (let i = 0; i < args.length; i += 1) {
10258
+ const word = args[i];
10259
+ if ((command === "script" || command === "su") && word === "-c") {
10260
+ const script = args[i + 1];
10261
+ return script === void 0 ? "exec" : capabilityFromWords(script.split(/\s+/).filter(Boolean));
10262
+ }
10263
+ if (word.startsWith("-")) continue;
10264
+ if (NUMERIC_PATTERN.test(word)) continue;
10265
+ if (command === "ssh") {
10266
+ const remote = args.slice(i + 1).filter((w) => !w.startsWith("-"));
10267
+ return remote.length === 0 ? "network" : capabilityFromWords(remote);
10268
+ }
10269
+ return capabilityFromWords(args.slice(i));
10270
+ }
10271
+ return "exec";
10272
+ }
10273
+ function capabilityOfHead(command, words) {
10274
+ if (DYNAMIC_WORD_PATTERN2.test(command) || command.includes(CMDSUBST_PLACEHOLDER)) return "dynamic";
10275
+ if (DYNAMIC_COMMANDS.has(command)) return "dynamic";
10276
+ if (command === "git" && words.slice(1).includes("-c")) return "unknown";
10277
+ if (command === "env" && words.includes("-S")) return "exec";
10278
+ if (LAUNCHER_COMMANDS.has(command)) return launcherCapability(command, words);
10279
+ if (INTERPRETER_COMMANDS.has(command)) return "exec";
10280
+ if (NETWORK_COMMANDS.has(command)) return "network";
10281
+ if (WRITE_CAPABILITY_COMMANDS.has(command)) return "write";
10282
+ return "unknown";
10283
+ }
10284
+ function isJudgedShellScript(head) {
10285
+ if (!SHELL_COMMANDS.has(head.command)) return false;
10286
+ return head.words.slice(1).some(
10287
+ (word) => word === "-c" || word.startsWith("--command") || word.startsWith("-") && !word.startsWith("--") && word.includes("c")
10288
+ );
10289
+ }
10290
+ function isInertSegment(segment, head) {
10291
+ if (!INERT_COMMANDS.has(head.command)) return false;
10292
+ const args = head.words.slice(1);
10293
+ if (head.command === "date" && args.some(isDateSetFlag)) return false;
10294
+ if (head.command === "command" && args[0] !== "-v" && args[0] !== "-V") return false;
10295
+ for (const token of segment.tokens) {
10296
+ if (token.kind === "redirect") return false;
10297
+ }
10298
+ for (const word of head.words) {
10299
+ if (word.includes(CMDSUBST_PLACEHOLDER) || /[$`]/.test(word)) return false;
10300
+ const value = stripAssignment(word);
10301
+ if (value.startsWith("/") || value.startsWith("~")) return false;
10302
+ }
10303
+ return true;
10304
+ }
10305
+ function segmentCapability(segment) {
10306
+ if (hasOutputRedirect(segment)) return "write";
10307
+ const head = headCommand(segment);
10308
+ if (head !== void 0 && isJudgedShellScript(head)) return "read";
10309
+ if (head !== void 0 && isWriteCommandHead(head.command, head.words)) return "write";
10310
+ if (head !== void 0 && isInertSegment(segment, head)) return "inert";
10311
+ if (isReadOnlySegment(segment)) return "read";
10312
+ if (head === void 0) {
10313
+ const words = wordTexts(segment);
10314
+ let i = 0;
10315
+ while (i < words.length && ASSIGNMENT_PATTERN2.test(words[i])) i += 1;
10316
+ const first = words[i];
10317
+ if (first === void 0) return "unknown";
10318
+ if (first === "env" && words.includes("-S")) return "exec";
10319
+ if (DYNAMIC_WORD_PATTERN2.test(first) || first.includes(CMDSUBST_PLACEHOLDER)) return "dynamic";
10320
+ return "unknown";
10321
+ }
10322
+ return capabilityOfHead(head.command, head.words);
10323
+ }
10324
+ function headWord(segment) {
10325
+ const head = headCommand(segment);
10326
+ if (head !== void 0) return head.command;
10327
+ const words = wordTexts(segment);
10328
+ return words.find((word) => !ASSIGNMENT_PATTERN2.test(word)) ?? words[0] ?? "\u2026";
10329
+ }
10330
+ function ask(matchedRule, reason) {
10331
+ return { decision: "ask", matchedRule, reason };
10332
+ }
10333
+ function evaluateCapabilityGate(scope, segments, cursor) {
10334
+ let execAsk;
10335
+ let networkAsk;
10336
+ let unknownAsk;
10337
+ for (const segment of segments) {
10338
+ const capability = segmentCapability(segment);
10339
+ if (capability === "inert") continue;
10340
+ if (capability === "dynamic") {
10341
+ const word = headWord(segment);
10342
+ const reason = DYNAMIC_COMMANDS.has(word) ? `\`${word}\` runs a script inside the current shell \u2014 controller approval is required` : `the command word "${word}" is dynamically constructed \u2014 controller approval is required`;
10343
+ return ask("dynamic-shell", reason);
10344
+ }
10345
+ if (capability === "write") {
10346
+ const head = headCommand(segment);
10347
+ const operands = relativeOperands(head !== void 0 ? head.words : wordTexts(segment));
10348
+ for (const operand of operands) {
10349
+ const abs = resolvePath(cursor, operand);
10350
+ if (!insideWorkspace(scope, abs)) {
10351
+ return ask(
10352
+ "write-scope",
10353
+ `write target "${operand}" resolves outside the workspace (${abs}) \u2014 controller approval is required`
10354
+ );
10355
+ }
10356
+ }
10357
+ continue;
10358
+ }
10359
+ if (capability === "read") {
10360
+ const head = headCommand(segment);
10361
+ const operands = head !== void 0 ? relativeOperands(head.words) : [];
10362
+ for (const operand of [...operands, ...relativeInputTargets(segment)]) {
10363
+ const abs = resolvePath(cursor, operand);
10364
+ const verdict = decidePath(scope, abs, "read");
10365
+ if (verdict.decision === "deny") return toToolDecision(verdict, "read");
10366
+ const narrowed = narrowHomeRead(verdict, { path: abs, access: "read" });
10367
+ if (narrowed.decision === "ask") {
10368
+ return { ...narrowed, askPaths: [narrowed.resolvedPath ?? abs] };
10369
+ }
10370
+ }
10371
+ continue;
10372
+ }
10373
+ if (capability === "exec") {
10374
+ execAsk ??= ask(
10375
+ "exec-capability",
10376
+ `\`${headWord(segment)}\` runs a shell or interpreter \u2014 controller approval is required (exec capability)`
10377
+ );
10378
+ } else if (capability === "network") {
10379
+ networkAsk ??= ask(
10380
+ "network-capability",
10381
+ `\`${headWord(segment)}\` reaches the network \u2014 controller approval is required (network capability)`
10382
+ );
10383
+ } else if (capability === "unknown") {
10384
+ unknownAsk ??= ask(
10385
+ "unknown-capability",
10386
+ `\`${headWord(segment)}\` is not on the bash capability tables \u2014 controller approval is required (unknown capability)`
10387
+ );
10388
+ }
10389
+ }
10390
+ return execAsk ?? networkAsk ?? unknownAsk;
10391
+ }
10392
+ function redirectWriteScopeAsk(scope, targets, cursor) {
10393
+ for (const target of targets) {
10394
+ if (target.startsWith("/") || target.startsWith("~")) continue;
10395
+ const abs = resolvePath(cursor, target);
10396
+ if (!insideWorkspace(scope, abs)) {
10397
+ return ask(
10398
+ "write-scope",
10399
+ `output redirection target "${target}" resolves outside the workspace (${abs}) \u2014 controller approval is required`
10400
+ );
10401
+ }
10402
+ }
10403
+ return void 0;
10404
+ }
10405
+
10406
+ // src/services/tools/permission-rules/bash-paths.ts
10407
+ function pathPieces(text, access) {
10408
+ const candidates = [];
10409
+ for (const piece of text.split("=")) {
10410
+ if (!piece.startsWith("/") && !piece.startsWith("~")) continue;
10411
+ if (isDiscardRedirectTarget(piece)) continue;
10412
+ candidates.push({ path: piece, access });
10413
+ }
10414
+ return candidates;
10415
+ }
10416
+ function extractPathCandidates(segments) {
10417
+ const candidates = [];
10418
+ for (const segment of segments) {
10419
+ const segmentLevel = segmentAccess(segment);
10420
+ for (const token of segment.tokens) {
10421
+ if (token.kind === "word") {
10422
+ candidates.push(...pathPieces(token.word.text, segmentLevel));
10423
+ continue;
10424
+ }
10425
+ const { redirect } = token;
10426
+ if (redirect.kind === "fd-dup" || redirect.kind === "here-doc" || redirect.kind === "here-string") continue;
10427
+ const target = redirect.target?.text;
10428
+ if (target === void 0 || isDiscardRedirectTarget(target)) continue;
10429
+ if (target.includes(CMDSUBST_PLACEHOLDER)) continue;
10430
+ const access = redirect.kind === "output" ? "write" : segmentLevel;
10431
+ for (const candidate of pathPieces(target, access)) {
10432
+ candidates.push(
10433
+ redirect.kind === "output" ? { ...candidate, reason: `output redirection to "${target}"` } : candidate
10434
+ );
10435
+ }
10436
+ }
10437
+ }
10438
+ return candidates;
10439
+ }
10440
+ function commandAccess(candidates) {
10441
+ let access = "unknown";
10442
+ for (const candidate of candidates) {
10443
+ if (candidate.access === "write") return "write";
10444
+ if (candidate.access === "read") access = "read";
10445
+ }
10446
+ return access;
10447
+ }
10448
+
10449
+ // src/services/tools/permission-rules/cd-chain.ts
10450
+ var NON_LITERAL_TARGET_CHARS = /[$`*?[\]{}]/;
10451
+ function literalCdTarget(segment) {
10452
+ if (segment.tokens.length !== 2) return void 0;
10453
+ const [command, argument] = segment.tokens;
10454
+ if (command.kind !== "word" || argument.kind !== "word") return void 0;
10455
+ if (command.word.text !== "cd") return void 0;
10456
+ if (command.word.substitutions.length > 0) return void 0;
10457
+ const target = argument.word.text;
10458
+ if (target === "" || target === "-") return void 0;
10459
+ if (argument.word.substitutions.length > 0) return void 0;
10460
+ if (target.includes(CMDSUBST_PLACEHOLDER)) return void 0;
10461
+ if (NON_LITERAL_TARGET_CHARS.test(target)) return void 0;
10462
+ return target;
10463
+ }
10464
+ function cdAskReason(scope, argument, abs, verdict) {
10465
+ if (verdict.sensitive === true && verdict.matchedEntry !== void 0) {
10466
+ return `cd target "${argument}" is the sensitive location ${verdict.matchedEntry} (${abs}) \u2014 controller approval is required`;
10467
+ }
10468
+ if (verdict.matchedRule === "read-within-home") {
10469
+ return `cd target "${argument}" is inside the home directory (${abs}) \u2014 controller approval is required`;
10470
+ }
10471
+ return `cd target "${argument}" resolves outside the allowed scope (${abs}) \u2014 controller approval is required`;
10472
+ }
10473
+ function applyCdChain(scope, segments) {
10474
+ let cwd = scope.workspace;
10475
+ let i = 0;
10476
+ while (i + 1 < segments.length && segments[i].operator === "&&") {
10477
+ const argument = literalCdTarget(segments[i]);
10478
+ if (argument === void 0) break;
10479
+ const expanded = expandHome(argument);
10480
+ if (expanded.unresolvable) break;
10481
+ const abs = normalizePath(expanded.path, { base: cwd });
10482
+ const verdict = decidePath(scope, abs, "read");
10483
+ if (verdict.decision === "deny") {
10484
+ return { segments: segments.slice(i), decision: toToolDecision(verdict, "read") };
10485
+ }
10486
+ const narrowed = narrowHomeRead(verdict, { path: abs, access: "read" });
10487
+ if (narrowed.decision === "ask") {
10488
+ return {
10489
+ segments: segments.slice(i),
10490
+ decision: { ...narrowed, reason: cdAskReason(scope, argument, abs, narrowed) }
10491
+ };
10492
+ }
10493
+ cwd = abs;
10494
+ i += 1;
10495
+ }
10496
+ return { segments: segments.slice(i), cwd };
10497
+ }
10498
+
10226
10499
  // src/services/tools/permission-rules/bash-access.ts
10227
10500
  function fallbackSensitiveHit(scope, expandedCommand) {
10228
10501
  for (const raw of expandedCommand.split(/[\s=]+/)) {
@@ -10274,14 +10547,14 @@ function evaluateBashCommand(scope, command, depth) {
10274
10547
  if (verdict.decision === "deny") return toToolDecision(verdict, candidate.access);
10275
10548
  const narrowed = narrowHomeRead(verdict, candidate);
10276
10549
  if (narrowed.decision === "ask") {
10277
- const ask = {
10550
+ const ask2 = {
10278
10551
  ...narrowed,
10279
10552
  ...candidate.reason !== void 0 && { reason: candidate.reason }
10280
10553
  };
10281
10554
  const askingPath = narrowed.resolvedPath ?? candidate.path;
10282
10555
  if (!askPaths.includes(askingPath)) askPaths.push(askingPath);
10283
- if (ask.sensitive === true) sensitiveAsk ??= ask;
10284
- else firstAsk ??= ask;
10556
+ if (ask2.sensitive === true) sensitiveAsk ??= ask2;
10557
+ else firstAsk ??= ask2;
10285
10558
  } else firstAllow ??= narrowed;
10286
10559
  }
10287
10560
  const lineAccess = candidates.length > 0 ? commandAccess(candidates) : segments.length > 0 && segments.every((segment) => segmentAccess(segment) === "read") ? "read" : "unknown";
@@ -10316,11 +10589,12 @@ function evaluateBashCommand(scope, command, depth) {
10316
10589
  };
10317
10590
  }
10318
10591
  if (redirections.writeTargets.length > 0 && !firstAsk && !sensitiveAsk) {
10319
- return {
10320
- decision: "ask",
10321
- matchedRule: "dynamic-shell",
10322
- reason: `output redirection writes a file ("${redirections.writeTargets.join('", "')}")`
10323
- };
10592
+ const scopeAsk = redirectWriteScopeAsk(
10593
+ scope,
10594
+ redirections.writeTargets,
10595
+ cdChain.cwd ?? scope.workspace
10596
+ );
10597
+ if (scopeAsk) return { ...scopeAsk, access: lineAccess };
10324
10598
  }
10325
10599
  if (hasUnresolvableHome) {
10326
10600
  return {
@@ -10352,6 +10626,8 @@ function evaluateBashCommand(scope, command, depth) {
10352
10626
  };
10353
10627
  }
10354
10628
  if (firstAsk) return firstAsk;
10629
+ const capabilityAsk = evaluateCapabilityGate(scope, segments, cdChain.cwd ?? scope.workspace);
10630
+ if (capabilityAsk) return { ...capabilityAsk, access: lineAccess };
10355
10631
  if (firstAllow) return firstAllow;
10356
10632
  return { decision: "allow", matchedRule: "no-path", access: lineAccess };
10357
10633
  }
@@ -10916,8 +11192,8 @@ var ClaudeWorkerBase = class {
10916
11192
  * Provide the engine-backed ask_controller implementation. Once set, the
10917
11193
  * worker exposes it to Claude Code as an in-process SDK MCP tool.
10918
11194
  */
10919
- setInquirySupport(ask) {
10920
- this._askController = ask;
11195
+ setInquirySupport(ask2) {
11196
+ this._askController = ask2;
10921
11197
  }
10922
11198
  flushMessageQueue() {
10923
11199
  this._messageQueue.length = 0;
@@ -11126,11 +11402,11 @@ var ClaudeWorkerPermissionGate = class _ClaudeWorkerPermissionGate extends Claud
11126
11402
  * times out → deny). Shared by every `ask` verdict so the request flow stays
11127
11403
  * identical to the puppet worker's.
11128
11404
  */
11129
- async requestPermission(toolName, input2, ask, grantKeys = []) {
11405
+ async requestPermission(toolName, input2, ask2, grantKeys = []) {
11130
11406
  if (!this._signalQueue || !this._runContext) return { behavior: "allow", updatedInput: input2 };
11131
11407
  const requestId = generateId("perm");
11132
- const description = this.describePermissionRequest(toolName, input2, ask);
11133
- const operation = ask.operation;
11408
+ const description = this.describePermissionRequest(toolName, input2, ask2);
11409
+ const operation = ask2.operation;
11134
11410
  this._signalQueue.enqueue({
11135
11411
  source: "worker",
11136
11412
  type: "permission_request",
@@ -11144,12 +11420,12 @@ var ClaudeWorkerPermissionGate = class _ClaudeWorkerPermissionGate extends Claud
11144
11420
  toolName,
11145
11421
  toolArgs: input2,
11146
11422
  description,
11147
- ...ask.matchedRule !== void 0 && { matchedRule: ask.matchedRule },
11148
- ...ask.matchedEntry !== void 0 && { matchedEntry: ask.matchedEntry },
11149
- ...ask.matchedSource !== void 0 && { matchedSource: ask.matchedSource },
11150
- ...ask.resolvedPath !== void 0 && { resolvedPath: ask.resolvedPath },
11151
- ...ask.sensitive !== void 0 && { sensitive: ask.sensitive },
11152
- ...ask.access !== void 0 && { access: ask.access },
11423
+ ...ask2.matchedRule !== void 0 && { matchedRule: ask2.matchedRule },
11424
+ ...ask2.matchedEntry !== void 0 && { matchedEntry: ask2.matchedEntry },
11425
+ ...ask2.matchedSource !== void 0 && { matchedSource: ask2.matchedSource },
11426
+ ...ask2.resolvedPath !== void 0 && { resolvedPath: ask2.resolvedPath },
11427
+ ...ask2.sensitive !== void 0 && { sensitive: ask2.sensitive },
11428
+ ...ask2.access !== void 0 && { access: ask2.access },
11153
11429
  operation
11154
11430
  },
11155
11431
  ...this._turnLineage
@@ -11164,12 +11440,12 @@ var ClaudeWorkerPermissionGate = class _ClaudeWorkerPermissionGate extends Claud
11164
11440
  recordPermissionAudit(this.logger, {
11165
11441
  decision: denied ? "denied" : "approved",
11166
11442
  operation,
11167
- access: auditAccessFor(ask.access),
11168
- resolvedPath: ask.resolvedPath,
11443
+ access: auditAccessFor(ask2.access),
11444
+ resolvedPath: ask2.resolvedPath,
11169
11445
  requestedPath: this.requestedPathOf(input2),
11170
- matchedEntry: ask.matchedEntry,
11171
- matchedSource: ask.matchedSource,
11172
- matchedRule: ask.matchedRule,
11446
+ matchedEntry: ask2.matchedEntry,
11447
+ matchedSource: ask2.matchedSource,
11448
+ matchedRule: ask2.matchedRule,
11173
11449
  taskId: this._runContext.taskId,
11174
11450
  workerId: this._runContext.workerId,
11175
11451
  toolName,
@@ -11185,7 +11461,7 @@ var ClaudeWorkerPermissionGate = class _ClaudeWorkerPermissionGate extends Claud
11185
11461
  return result2;
11186
11462
  }
11187
11463
  /** Human-readable summary of a gated call for the approval prompt. */
11188
- describePermissionRequest(toolName, input2, ask) {
11464
+ describePermissionRequest(toolName, input2, ask2) {
11189
11465
  let detail;
11190
11466
  if (toolName === "Bash") {
11191
11467
  detail = `Execute shell command: ${String(input2.command ?? "")}`;
@@ -11198,15 +11474,15 @@ var ClaudeWorkerPermissionGate = class _ClaudeWorkerPermissionGate extends Claud
11198
11474
  } else {
11199
11475
  detail = `${toolName}: ${JSON.stringify(input2)}`;
11200
11476
  }
11201
- const hit = ask.sensitive ? verdictDetailSuffix({
11202
- matchedRule: ask.matchedRule,
11203
- matchedEntry: ask.matchedEntry,
11204
- matchedSource: ask.matchedSource,
11205
- resolvedPath: ask.resolvedPath,
11206
- operation: ask.operation
11477
+ const hit = ask2.sensitive ? verdictDetailSuffix({
11478
+ matchedRule: ask2.matchedRule,
11479
+ matchedEntry: ask2.matchedEntry,
11480
+ matchedSource: ask2.matchedSource,
11481
+ resolvedPath: ask2.resolvedPath,
11482
+ operation: ask2.operation
11207
11483
  }) : "";
11208
11484
  const withHit = `${detail}${hit}`;
11209
- return ask.reason ? `${withHit} (${ask.reason})` : withHit;
11485
+ return ask2.reason ? `${withHit} (${ask2.reason})` : withHit;
11210
11486
  }
11211
11487
  };
11212
11488
 
@@ -13120,9 +13396,9 @@ function mcpToolIdentity(name) {
13120
13396
  const lower = name.trim().toLowerCase();
13121
13397
  if (lower.startsWith("mcp__")) {
13122
13398
  const rest = lower.slice("mcp__".length);
13123
- const sep2 = rest.lastIndexOf("__");
13124
- if (sep2 <= 0 || sep2 + 2 >= rest.length) return void 0;
13125
- return { server: rest.slice(0, sep2), tool: rest.slice(sep2 + 2) };
13399
+ const sep3 = rest.lastIndexOf("__");
13400
+ if (sep3 <= 0 || sep3 + 2 >= rest.length) return void 0;
13401
+ return { server: rest.slice(0, sep3), tool: rest.slice(sep3 + 2) };
13126
13402
  }
13127
13403
  const qualified = /^([a-z0-9_]+)[.-]([a-z0-9_]+)$/.exec(lower);
13128
13404
  return qualified ? { server: qualified[1], tool: qualified[2] } : void 0;
@@ -13225,7 +13501,7 @@ function bridgeToolArgs(snapshot) {
13225
13501
  }
13226
13502
  return { locations: snapshot.locations ?? [] };
13227
13503
  }
13228
- function describeRequest(snapshot, classification, ask) {
13504
+ function describeRequest(snapshot, classification, ask2) {
13229
13505
  let detail;
13230
13506
  if (classification.type === "policy" && classification.command !== void 0) {
13231
13507
  detail = `Execute shell command: ${classification.command}`;
@@ -13234,15 +13510,15 @@ function describeRequest(snapshot, classification, ask) {
13234
13510
  } else {
13235
13511
  detail = `${bridgeToolName(snapshot)}: ${JSON.stringify(bridgeToolArgs(snapshot))}`;
13236
13512
  }
13237
- const hit = ask?.sensitive ? verdictDetailSuffix({
13238
- matchedRule: ask.matchedRule,
13239
- matchedEntry: ask.matchedEntry,
13240
- matchedSource: ask.matchedSource,
13241
- resolvedPath: ask.resolvedPath,
13242
- operation: ask.operation
13513
+ const hit = ask2?.sensitive ? verdictDetailSuffix({
13514
+ matchedRule: ask2.matchedRule,
13515
+ matchedEntry: ask2.matchedEntry,
13516
+ matchedSource: ask2.matchedSource,
13517
+ resolvedPath: ask2.resolvedPath,
13518
+ operation: ask2.operation
13243
13519
  }) : "";
13244
13520
  const withHit = `${detail}${hit}`;
13245
- return ask?.reason ? `${withHit} (${ask.reason})` : withHit;
13521
+ return ask2?.reason ? `${withHit} (${ask2.reason})` : withHit;
13246
13522
  }
13247
13523
  function createAcpPermissionBridge(deps, pendingPermissions) {
13248
13524
  const permissionMs = deps.permissionMs ?? ACP_PERMISSION_TIMEOUT_MS;
@@ -13295,7 +13571,7 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13295
13571
  if (classification.type === "direct-allow") {
13296
13572
  return outcomeFor("allow");
13297
13573
  }
13298
- const ask = classification.type === "direct-ask" ? { reason: classification.reason, operation: classification.operation } : { operation: operationForTool(classification.toolName) };
13574
+ const ask2 = classification.type === "direct-ask" ? { reason: classification.reason, operation: classification.operation } : { operation: operationForTool(classification.toolName) };
13299
13575
  if (classification.type === "policy") {
13300
13576
  const verdict = decideToolAccess(deps.scope, classification.toolName, classification.args);
13301
13577
  if (verdict.decision === "allow") {
@@ -13310,14 +13586,14 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13310
13586
  });
13311
13587
  return outcomeFor("allow");
13312
13588
  }
13313
- ask.reason = verdict.reason;
13314
- ask.matchedRule = verdict.matchedRule;
13315
- ask.matchedEntry = verdict.matchedEntry;
13316
- ask.matchedSource = verdict.matchedSource;
13317
- ask.resolvedPath = verdict.resolvedPath;
13318
- ask.sensitive = verdict.sensitive;
13319
- ask.access = verdict.access;
13320
- ask.askPaths = verdict.askPaths;
13589
+ ask2.reason = verdict.reason;
13590
+ ask2.matchedRule = verdict.matchedRule;
13591
+ ask2.matchedEntry = verdict.matchedEntry;
13592
+ ask2.matchedSource = verdict.matchedSource;
13593
+ ask2.resolvedPath = verdict.resolvedPath;
13594
+ ask2.sensitive = verdict.sensitive;
13595
+ ask2.access = verdict.access;
13596
+ ask2.askPaths = verdict.askPaths;
13321
13597
  if (verdict.decision === "deny") {
13322
13598
  audit({
13323
13599
  ...base,
@@ -13358,14 +13634,14 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13358
13634
  emoji: deps.runContext.emoji,
13359
13635
  toolName,
13360
13636
  toolArgs: bridgeToolArgs(snapshot),
13361
- description: describeRequest(snapshot, classification, ask),
13362
- ...ask.matchedRule !== void 0 && { matchedRule: ask.matchedRule },
13363
- ...ask.matchedEntry !== void 0 && { matchedEntry: ask.matchedEntry },
13364
- ...ask.matchedSource !== void 0 && { matchedSource: ask.matchedSource },
13365
- ...ask.resolvedPath !== void 0 && { resolvedPath: ask.resolvedPath },
13366
- ...ask.sensitive !== void 0 && { sensitive: ask.sensitive },
13367
- ...ask.access !== void 0 && { access: ask.access },
13368
- operation: ask.operation
13637
+ description: describeRequest(snapshot, classification, ask2),
13638
+ ...ask2.matchedRule !== void 0 && { matchedRule: ask2.matchedRule },
13639
+ ...ask2.matchedEntry !== void 0 && { matchedEntry: ask2.matchedEntry },
13640
+ ...ask2.matchedSource !== void 0 && { matchedSource: ask2.matchedSource },
13641
+ ...ask2.resolvedPath !== void 0 && { resolvedPath: ask2.resolvedPath },
13642
+ ...ask2.sensitive !== void 0 && { sensitive: ask2.sensitive },
13643
+ ...ask2.access !== void 0 && { access: ask2.access },
13644
+ operation: ask2.operation
13369
13645
  },
13370
13646
  ...deps.turnLineage ?? {}
13371
13647
  });
@@ -13376,11 +13652,11 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13376
13652
  audit({
13377
13653
  ...base,
13378
13654
  decision: result3.approved ? "approved" : "denied",
13379
- access: ask.access !== void 0 ? auditAccessFor(ask.access) : "unknown",
13380
- resolvedPath: ask.resolvedPath,
13381
- matchedRule: ask.matchedRule,
13382
- matchedEntry: ask.matchedEntry,
13383
- matchedSource: ask.matchedSource,
13655
+ access: ask2.access !== void 0 ? auditAccessFor(ask2.access) : "unknown",
13656
+ resolvedPath: ask2.resolvedPath,
13657
+ matchedRule: ask2.matchedRule,
13658
+ matchedEntry: ask2.matchedEntry,
13659
+ matchedSource: ask2.matchedSource,
13384
13660
  ...!result3.approved && { source: denialSource(result3.reason) }
13385
13661
  });
13386
13662
  if (result3.approved) {
@@ -13389,7 +13665,7 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13389
13665
  }
13390
13666
  return outcomeFor("allow");
13391
13667
  }
13392
- emitWaitDenial(result3.reason, toolName, denialTarget, ask.matchedRule);
13668
+ emitWaitDenial(result3.reason, toolName, denialTarget, ask2.matchedRule);
13393
13669
  return outcomeFor("reject");
13394
13670
  }
13395
13671
  const requestId = generateId("perm");
@@ -13405,8 +13681,8 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13405
13681
  emoji: deps.runContext.emoji,
13406
13682
  toolName,
13407
13683
  toolArgs: bridgeToolArgs(snapshot),
13408
- description: describeRequest(snapshot, classification, ask),
13409
- operation: ask.operation
13684
+ description: describeRequest(snapshot, classification, ask2),
13685
+ operation: ask2.operation
13410
13686
  },
13411
13687
  ...deps.turnLineage ?? {}
13412
13688
  });
@@ -13420,7 +13696,7 @@ function createAcpPermissionBridge(deps, pendingPermissions) {
13420
13696
  access: "unknown",
13421
13697
  ...!result2.approved && { source: denialSource(result2.reason) }
13422
13698
  });
13423
- if (!result2.approved) emitWaitDenial(result2.reason, toolName, denialTarget, ask.matchedRule);
13699
+ if (!result2.approved) emitWaitDenial(result2.reason, toolName, denialTarget, ask2.matchedRule);
13424
13700
  return result2.approved ? outcomeFor("allow") : outcomeFor("reject");
13425
13701
  };
13426
13702
  }
@@ -14001,8 +14277,8 @@ var BaseAcpWorker = class {
14001
14277
  * MCP bridge serves it over a Unix socket (member presence is what the
14002
14278
  * engine's `wireWorker` probe detects, as with ClaudeCodeWorker).
14003
14279
  */
14004
- setInquirySupport(ask) {
14005
- this._askController = ask;
14280
+ setInquirySupport(ask2) {
14281
+ this._askController = ask2;
14006
14282
  }
14007
14283
  getState() {
14008
14284
  return this.state;
@@ -15064,15 +15340,15 @@ ${joinControllerMessages(initialQueued)}`;
15064
15340
  }
15065
15341
  /** ask_controller: same blocking engine channel as ClaudeCodeWorker, with an audit entry. */
15066
15342
  async mcpAsk(question, context) {
15067
- const ask = this._askController;
15068
- if (!ask || !this._mcpHost?.mcpAvailable) {
15343
+ const ask2 = this._askController;
15344
+ if (!ask2 || !this._mcpHost?.mcpAvailable) {
15069
15345
  return "[UNAVAILABLE] The controller inquiry channel is not available for this worker.";
15070
15346
  }
15071
15347
  const q = capText(question, MAX_QUESTION_CHARS);
15072
15348
  const c = context === void 0 ? void 0 : capText(context, MAX_CONTEXT_CHARS);
15073
15349
  let answer;
15074
15350
  try {
15075
- answer = await ask(q, c);
15351
+ answer = await ask2(q, c);
15076
15352
  } catch (err) {
15077
15353
  answer = `[UNAVAILABLE] ${err instanceof Error ? err.message : String(err)}`;
15078
15354
  }
@@ -6,7 +6,7 @@ import { connect } from "net";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "wayang-ai",
9
- version: "0.9.5",
9
+ version: "0.9.6",
10
10
  description: "Multi-agent orchestration platform",
11
11
  type: "module",
12
12
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayang-ai",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Multi-agent orchestration platform",
5
5
  "type": "module",
6
6
  "license": "MIT",