scream-code 0.18.1 → 0.18.2

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.
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
7
  import { A as sharedMemoStore, B as buildProjectTagCloud, C as applyConsolidation, D as parseMemoryMemos, E as buildExitExtractionPrompt, F as probeLocalEmbeddingSupport, G as join$1, H as basename$1, I as createMemoryMemo, J as relative$1, K as normalize, L as toSummary, M as clearEmbeddingModelCache, N as createFastEmbedEngine, O as stripMemoryTags, P as hasEmbeddingModelCache$1, R as generateTags, S as DreamTracker, T as EXIT_EXTRACTION_SYSTEM_PROMPT, U as dirname$2, V as rankMemos, W as isAbsolute$1, Y as resolve$2, a as isSupportedFile, b as sharedKnowledgeStoreReady, i as ingestFile, j as classifyEmbeddingFailure, k as MemoryMemoStore, q as parse$7, r as ingestDirectory, t as multiSearch, w as buildConsolidationPlan, y as sharedKnowledgeStore, z as normalizeTags } from "./src-C7C9e5Ds.mjs";
8
8
  import { n as require_base64_js, t as XMLParser } from "./XMLParser-DFAnGqkc.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-Bu8GPguU.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-Dw-Cl3DW.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -68001,7 +68001,10 @@ const PermissionRuleSchema = z.object({
68001
68001
  pattern: z.string().min(1).refine(isValidPermissionPattern, { message: "Invalid permission rule pattern" }),
68002
68002
  reason: z.string().optional()
68003
68003
  });
68004
- const PermissionConfigSchema = z.object({ rules: z.array(PermissionRuleSchema).optional() });
68004
+ const PermissionConfigSchema = z.object({
68005
+ rules: z.array(PermissionRuleSchema).optional(),
68006
+ egressAllowlist: z.array(z.string()).optional()
68007
+ });
68005
68008
  const LoopControlSchema = z.object({
68006
68009
  maxStepsPerTurn: z.number().int().min(0).optional(),
68007
68010
  maxRetriesPerStep: z.number().int().min(0).optional(),
@@ -87483,6 +87486,216 @@ var AutoModeAskUserQuestionDenyPermissionPolicy = class {
87483
87486
  }
87484
87487
  };
87485
87488
  //#endregion
87489
+ //#region ../../packages/agent-core/src/agent/permission/policies/private-read-egress-ask.ts
87490
+ /**
87491
+ * Hosts that may receive data even after the session read private data from
87492
+ * outside the workspace. Merged (union) with the user's configured
87493
+ * `permission.egressAllowlist`.
87494
+ */
87495
+ const DEFAULT_EGRESS_ALLOWLIST = [
87496
+ "github.com",
87497
+ "api.github.com",
87498
+ "raw.githubusercontent.com",
87499
+ "registry.npmjs.org",
87500
+ "pypi.org",
87501
+ "files.pythonhosted.org",
87502
+ "wikipedia.org",
87503
+ "localhost",
87504
+ "127.0.0.1",
87505
+ "[::1]"
87506
+ ];
87507
+ /**
87508
+ * Shared guard condition: returns an assessment when this call would move
87509
+ * private-read data to a destination that is not allowlisted, undefined when
87510
+ * the call is clean (or not an egress call at all).
87511
+ *
87512
+ * Two callers keep the same semantics:
87513
+ * - `PrivateReadEgressAskPermissionPolicy` — prompt the user (once or
87514
+ * session), even in auto/yolo mode;
87515
+ * - bot mode — deny instead, because no human is there to answer a prompt.
87516
+ *
87517
+ * Fail-closed details:
87518
+ * - a candidate private read that has not settled yet counts as taint: every
87519
+ * call in one tool batch is authorized before any result is finalized, so
87520
+ * a same-batch `[Read /tmp/secret, Bash(curl …)]` must not slip through;
87521
+ * - every destination found in the call must be allowlisted — one unknown
87522
+ * host in a command is enough to ask;
87523
+ * - a command whose destination cannot be extracted counts as unrecognized,
87524
+ * never as allowlisted.
87525
+ */
87526
+ function assessPrivateReadEgress(agent, context) {
87527
+ const permission = agent.permission;
87528
+ if (permission.taintedPrivateReads.length === 0 && !permission.hasPendingPrivateReads()) return;
87529
+ const hosts = egressHosts(context);
87530
+ if (hosts === void 0) return void 0;
87531
+ if (hasMatchingSessionGrant(agent, context)) return void 0;
87532
+ if (hosts.length === 0) return {
87533
+ reason: egressReason(void 0),
87534
+ targetKnown: false
87535
+ };
87536
+ const allowlist = egressAllowlist(agent);
87537
+ const blocked = hosts.find((host) => !isAllowedEgressHost(host, allowlist));
87538
+ if (blocked === void 0) return void 0;
87539
+ return {
87540
+ reason: egressReason(blocked),
87541
+ targetKnown: true
87542
+ };
87543
+ }
87544
+ /**
87545
+ * Once the session has successfully read data from outside the workspace,
87546
+ * sending anything to a host outside the allowlist asks first — the data may
87547
+ * have come from a private file, and an unrecognized destination cannot be
87548
+ * assumed safe.
87549
+ *
87550
+ * The chain position is the mechanism (see `createPermissionDecisionPolicies`):
87551
+ * the policy sits after `collaboration-auto-approve` and before
87552
+ * `auto-mode-approve`, so it fires even in unattended auto mode; the allowlist,
87553
+ * not the chain, is what keeps ordinary auto-mode work quiet. It sits before
87554
+ * `session-approval-history` as well, so only a memorized grant that matches
87555
+ * this exact call can silence it.
87556
+ *
87557
+ * Egress detection is deliberately narrow (MVP): `FetchURL` and the shell
87558
+ * download/remote verbs in `Bash`. Known blind spots, kept here so the next
87559
+ * batch starts from the list: egress through python, RunScript or MCP tools;
87560
+ * shell reads without a read verb or without an absolute path — `cd / && cat
87561
+ * .env`, `curl -d @file`, `-T`, `dd`; `WebSearch`/`PaperSearch` are not judged
87562
+ * as egress either, so a tainted session still reaches the network through
87563
+ * them (consistent with the allowlist being about hosts, registered as an
87564
+ * observation); and encoding tricks that hide a host or a path from the
87565
+ * command text.
87566
+ */
87567
+ var PrivateReadEgressAskPermissionPolicy = class {
87568
+ agent;
87569
+ name = "private-read-egress-ask";
87570
+ constructor(agent) {
87571
+ this.agent = agent;
87572
+ }
87573
+ evaluate(context) {
87574
+ const assessment = assessPrivateReadEgress(this.agent, context);
87575
+ if (assessment === void 0) return;
87576
+ return {
87577
+ kind: "ask",
87578
+ reason: {
87579
+ private_read_egress: true,
87580
+ target_known: assessment.targetKnown
87581
+ },
87582
+ reasons: [assessment.reason],
87583
+ grantOptions: ["once", "session"]
87584
+ };
87585
+ }
87586
+ };
87587
+ /** Built-in hosts plus the configured ones; configuration only widens. */
87588
+ function egressAllowlist(agent) {
87589
+ const configured = agent.screamConfig?.permission?.egressAllowlist;
87590
+ if (configured === void 0 || configured.length === 0) return DEFAULT_EGRESS_ALLOWLIST;
87591
+ return [...DEFAULT_EGRESS_ALLOWLIST, ...configured];
87592
+ }
87593
+ /** A memorized approve-for-session grant that matches this call wins. */
87594
+ function hasMatchingSessionGrant(agent, context) {
87595
+ return agent.permission.sessionApprovalRulePatterns.some((pattern) => matchPermissionRule({
87596
+ rule: {
87597
+ decision: "allow",
87598
+ scope: "session-runtime",
87599
+ pattern,
87600
+ reason: "approve for session"
87601
+ },
87602
+ toolName: context.toolCall.name,
87603
+ execution: context.execution
87604
+ }) !== void 0);
87605
+ }
87606
+ /**
87607
+ * Destinations this call sends to, or undefined when the call is not egress:
87608
+ *
87609
+ * - `FetchURL` — the URL argument; an unparsable URL yields no destination
87610
+ * (unrecognized).
87611
+ * - `Bash` — a download/remote verb (curl, wget, nc, netcat, ssh, scp,
87612
+ * rsync, telnet) marks the call as egress; every host the command spells
87613
+ * out is collected, so a second URL cannot hide behind an allowlisted one.
87614
+ */
87615
+ function egressHosts(context) {
87616
+ if (context.toolCall.name === "FetchURL") {
87617
+ const url = context.args?.url;
87618
+ if (typeof url !== "string") return void 0;
87619
+ const host = hostFromUrl(url);
87620
+ return host === void 0 ? [] : [host];
87621
+ }
87622
+ if (context.toolCall.name === "Bash") {
87623
+ const command = context.args?.command;
87624
+ if (typeof command !== "string") return void 0;
87625
+ if (!EGRESS_COMMAND_PATTERN.test(command)) return void 0;
87626
+ return hostsFromCommand(command);
87627
+ }
87628
+ }
87629
+ /**
87630
+ * Shell download/remote verbs. Shared with the private-read taint scan
87631
+ * (`permission/index.ts`): an egress command reads local files through upload
87632
+ * flags (`-d @file`, `-T`) without any read verb, so it must not be filtered
87633
+ * out by that scan's read-verb gate.
87634
+ */
87635
+ const EGRESS_COMMAND_PATTERN = /\b(curl|wget|nc|netcat|ssh(?!-)|scp|rsync|telnet)\b/;
87636
+ /**
87637
+ * Authority of a URL. The character class stops at `/`, `#`, `?`, quotes,
87638
+ * backticks and whitespace, so a fragment or a path cannot extend the
87639
+ * authority into a different host.
87640
+ */
87641
+ const URL_AUTHORITY = /https?:\/\/([^\s/'"`#?]+)/gi;
87642
+ /** `user@host` (ssh/scp style). */
87643
+ const USER_HOST = /[\w.-]+@([A-Za-z0-9][A-Za-z0-9.-]*)/g;
87644
+ /** `host:path` remote targets (scp/rsync style). */
87645
+ const HOST_PATH = /(?:^|\s)([A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,}):\S/g;
87646
+ function hostsFromCommand(command) {
87647
+ const hosts = /* @__PURE__ */ new Set();
87648
+ for (const match of command.matchAll(URL_AUTHORITY)) {
87649
+ const host = hostFromAuthority(match[1] ?? "");
87650
+ if (host !== void 0) hosts.add(host);
87651
+ }
87652
+ for (const match of command.matchAll(USER_HOST)) {
87653
+ const host = match[1]?.toLowerCase();
87654
+ if (host !== void 0 && host.length > 0) hosts.add(host);
87655
+ }
87656
+ for (const match of command.matchAll(HOST_PATH)) {
87657
+ const host = match[1]?.toLowerCase();
87658
+ if (host !== void 0 && host.length > 0) hosts.add(host);
87659
+ }
87660
+ return [...hosts];
87661
+ }
87662
+ function hostFromUrl(url) {
87663
+ try {
87664
+ const hostname = new URL(url).hostname.toLowerCase();
87665
+ return hostname.length > 0 ? hostname : void 0;
87666
+ } catch {
87667
+ return;
87668
+ }
87669
+ }
87670
+ function hostFromAuthority(authority) {
87671
+ const withoutUser = authority.slice(authority.indexOf("@") + 1);
87672
+ if (withoutUser.startsWith("[")) {
87673
+ const end = withoutUser.indexOf("]");
87674
+ return end === -1 ? void 0 : withoutUser.slice(0, end + 1).toLowerCase();
87675
+ }
87676
+ const host = withoutUser.split(":")[0]?.toLowerCase();
87677
+ return host !== void 0 && host.length > 0 ? host : void 0;
87678
+ }
87679
+ /**
87680
+ * Case-insensitive exact match; a `*.` entry also covers its apex
87681
+ * (`*.example.com` matches `example.com` and `a.example.com`).
87682
+ */
87683
+ function isAllowedEgressHost(host, allowlist) {
87684
+ const normalized = host.toLowerCase();
87685
+ return allowlist.some((entry) => {
87686
+ const candidate = entry.trim().toLowerCase();
87687
+ if (candidate.length === 0) return false;
87688
+ if (candidate.startsWith("*.")) {
87689
+ const suffix = candidate.slice(2);
87690
+ return normalized === suffix || normalized.endsWith(`.${suffix}`);
87691
+ }
87692
+ return normalized === candidate;
87693
+ });
87694
+ }
87695
+ function egressReason(host) {
87696
+ return `private data was read outside the workspace in this session; sending it to ${host ?? "an unrecognized destination"} needs your confirmation`;
87697
+ }
87698
+ //#endregion
87486
87699
  //#region ../../packages/agent-core/src/agent/permission/policies/bot-mode-permission.ts
87487
87700
  /**
87488
87701
  * Tools whose effects are reversible within the workspace (safe to run
@@ -87585,10 +87798,16 @@ var BotModePermissionPolicy = class BotModePermissionPolicy {
87585
87798
  reason: { reason: "bot: in-workspace reversible edit" }
87586
87799
  };
87587
87800
  }
87588
- if (REVERSIBLE_TOOLS.has(tool)) return {
87589
- kind: "approve",
87590
- reason: { reason: "bot: reversible tool" }
87591
- };
87801
+ if (REVERSIBLE_TOOLS.has(tool)) {
87802
+ if (assessPrivateReadEgress(this.agent, context) !== void 0) return {
87803
+ kind: "deny",
87804
+ reason: { reason: "bot: egress after private read blocked (unattended)" }
87805
+ };
87806
+ return {
87807
+ kind: "approve",
87808
+ reason: { reason: "bot: reversible tool" }
87809
+ };
87810
+ }
87592
87811
  return {
87593
87812
  kind: "deny",
87594
87813
  reason: { reason: `bot: tool '${tool}' not in the reversible allowlist` }
@@ -88366,6 +88585,99 @@ function formatPermissionRuleDenyMessage(tool, reason, agentType) {
88366
88585
  return `Tool "${tool}" was denied by permission rule.${suffix}`;
88367
88586
  }
88368
88587
  //#endregion
88588
+ //#region ../../packages/agent-core/src/agent/permission/warnings.ts
88589
+ /**
88590
+ * Declaration order is the display order, so the riskiest families come first.
88591
+ * Every warning is a standalone English sentence; consumers render them as-is.
88592
+ */
88593
+ const DANGEROUS_COMMAND_PATTERNS = [
88594
+ {
88595
+ pattern: /\brm\s+(?=[^|;&\n]*(?<![\w])(?:-[a-zA-Z]*r[a-zA-Z]*\b|--recursive\b))(?=[^|;&\n]*(?<![\w])(?:-[a-zA-Z]*f[a-zA-Z]*\b|--force\b))/i,
88596
+ warning: "dangerous command: recursive force delete"
88597
+ },
88598
+ {
88599
+ pattern: /\bsudo\b/,
88600
+ warning: "dangerous command: privilege escalation"
88601
+ },
88602
+ {
88603
+ pattern: /\bmkfs\b/,
88604
+ warning: "dangerous command: filesystem format"
88605
+ },
88606
+ {
88607
+ pattern: /\bdd\s+if=/,
88608
+ warning: "dangerous command: raw disk write"
88609
+ },
88610
+ {
88611
+ pattern: /curl[^|]*\|\s*(ba)?sh|wget[^|]*\|\s*(ba)?sh/,
88612
+ warning: "dangerous command: download piped into a shell"
88613
+ },
88614
+ {
88615
+ pattern: /\bchmod\s+-R\s+777\b/,
88616
+ warning: "dangerous command: world-writable recursive chmod"
88617
+ },
88618
+ {
88619
+ pattern: />\s*\/dev\/sd/,
88620
+ warning: "dangerous command: write to a raw disk device"
88621
+ },
88622
+ {
88623
+ pattern: /\bshutdown\b/,
88624
+ warning: "dangerous command: system shutdown"
88625
+ },
88626
+ {
88627
+ pattern: /\breboot\b/,
88628
+ warning: "dangerous command: system reboot"
88629
+ },
88630
+ {
88631
+ pattern: /\bgit\s+(?:-C\s+\S+\s+)?push\b.*(?:\s-f\b|--force\b)/,
88632
+ warning: "dangerous command: force push"
88633
+ }
88634
+ ];
88635
+ /**
88636
+ * Warnings triggered by `command`, in declaration order. Returns an empty
88637
+ * array for ordinary commands, and for input that is not a command string.
88638
+ */
88639
+ function dangerousCommandWarnings(command) {
88640
+ if (command.length === 0) return [];
88641
+ const warnings = [];
88642
+ for (const { pattern, warning } of DANGEROUS_COMMAND_PATTERNS) if (pattern.test(command)) warnings.push(warning);
88643
+ return warnings;
88644
+ }
88645
+ //#endregion
88646
+ //#region ../../packages/agent-core/src/agent/permission/policies/warnings-ask.ts
88647
+ /**
88648
+ * Dangerous shell commands always ask — even in `yolo` mode and even when a
88649
+ * session grant already covers Bash.
88650
+ *
88651
+ * The chain position is the mechanism (see `createPermissionDecisionPolicies`):
88652
+ * the policy sits after `auto-mode-approve`, so unattended auto mode keeps
88653
+ * running without prompts, and before `session-approval-history`, so a
88654
+ * memorized grant cannot silence the warning. The prompt offers `once` only —
88655
+ * a dangerous command is never memorizable.
88656
+ */
88657
+ var WarningsAskPermissionPolicy = class {
88658
+ agent;
88659
+ name = "dangerous-command-warnings-ask";
88660
+ constructor(agent) {
88661
+ this.agent = agent;
88662
+ }
88663
+ evaluate(context) {
88664
+ if (context.toolCall.name !== "Bash") return;
88665
+ const command = context.args?.command;
88666
+ if (typeof command !== "string") return;
88667
+ const warnings = dangerousCommandWarnings(command);
88668
+ if (warnings.length === 0) return;
88669
+ return {
88670
+ kind: "ask",
88671
+ reason: {
88672
+ dangerous_command: true,
88673
+ warning_count: warnings.length
88674
+ },
88675
+ reasons: warnings,
88676
+ grantOptions: ["once"]
88677
+ };
88678
+ }
88679
+ };
88680
+ //#endregion
88369
88681
  //#region ../../packages/agent-core/src/agent/permission/policies/collaboration-auto-approve.ts
88370
88682
  /**
88371
88683
  * Coordination tools are conversation, not mutation: a subagent asking its
@@ -88435,7 +88747,9 @@ function createPermissionDecisionPolicies(agent) {
88435
88747
  new UserConfiguredDenyPermissionPolicy(agent),
88436
88748
  new BotModePermissionPolicy(agent),
88437
88749
  new CollaborationAutoApprovePermissionPolicy(),
88750
+ new PrivateReadEgressAskPermissionPolicy(agent),
88438
88751
  new AutoModeApprovePermissionPolicy(agent),
88752
+ new WarningsAskPermissionPolicy(agent),
88439
88753
  new SessionApprovalHistoryPermissionPolicy(agent),
88440
88754
  new UserConfiguredAskPermissionPolicy(agent),
88441
88755
  new UserConfiguredAllowPermissionPolicy(agent),
@@ -88464,6 +88778,15 @@ var PermissionManager = class {
88464
88778
  localSessionApprovalRulePatterns = /* @__PURE__ */ new Set();
88465
88779
  policies;
88466
88780
  pendingApprovals = /* @__PURE__ */ new Map();
88781
+ /**
88782
+ * Two-phase private-read tracking (see `trackPendingPrivateRead` /
88783
+ * `settlePrivateRead`): the first phase records a candidate read of
88784
+ * outside-workspace data keyed by tool call id, the second promotes it to
88785
+ * `localTaintedPrivateReads` only after the call is known to have
88786
+ * succeeded. Both maps are in-memory session state — never persisted.
88787
+ */
88788
+ localPendingPrivateReads = /* @__PURE__ */ new Map();
88789
+ localTaintedPrivateReads = /* @__PURE__ */ new Set();
88467
88790
  nextApprovalId = 0;
88468
88791
  constructor(agent, options = {}) {
88469
88792
  this.agent = agent;
@@ -88547,10 +88870,89 @@ var PermissionManager = class {
88547
88870
  get sessionApprovalRulePatterns() {
88548
88871
  return [...this.localSessionApprovalRulePatterns, ...this.parent?.sessionApprovalRulePatterns ?? []];
88549
88872
  }
88873
+ /**
88874
+ * Forget one memorized approve-for-session pattern. Revocation is local
88875
+ * only: `sessionApprovalRulePatterns` merges the parent chain read-only, so
88876
+ * this manager cannot drop a grant its parent holds (and a child's grant is
88877
+ * invisible to the parent). Returns true when a local grant was removed.
88878
+ *
88879
+ * A real removal is recorded on the wire, otherwise a resumed session would
88880
+ * re-add the pattern from its own approval-result record and prompt again
88881
+ * for a grant the user took back. The same method serves the replay path:
88882
+ * `AgentRecords.logRecord` is a no-op while restoring, so restore cannot
88883
+ * re-write the record it is replaying.
88884
+ */
88885
+ revokeSessionGrant(pattern) {
88886
+ const removed = this.localSessionApprovalRulePatterns.delete(pattern);
88887
+ if (removed) this.agent.records.logRecord({
88888
+ type: "permission.record_grant_revocation",
88889
+ pattern
88890
+ });
88891
+ return removed;
88892
+ }
88893
+ /**
88894
+ * Descriptions of the successful reads of outside-workspace data this
88895
+ * session has performed, merged with the parent chain read-only (same
88896
+ * shape as `sessionApprovalRulePatterns`). A subagent sees its parent's
88897
+ * taint but cannot add to it, and the taint dies with the agent: it is
88898
+ * never persisted.
88899
+ */
88900
+ get taintedPrivateReads() {
88901
+ return [...this.localTaintedPrivateReads, ...this.parent?.taintedPrivateReads ?? []];
88902
+ }
88903
+ /**
88904
+ * True while a candidate private read has not settled yet, on this agent or
88905
+ * anywhere up the parent chain. Every call in one tool batch is authorized
88906
+ * before any of their results is finalized, so a same-batch
88907
+ * `[Read /tmp/secret, Bash(curl …)]` never sees the taint — the egress
88908
+ * guard reads this flag too and fails closed during that window.
88909
+ */
88910
+ hasPendingPrivateReads() {
88911
+ return this.localPendingPrivateReads.size > 0 || (this.parent?.hasPendingPrivateReads() ?? false);
88912
+ }
88913
+ /**
88914
+ * Second half of two-phase private-read tracking. A pending candidate read
88915
+ * is promoted to the tainted set only when the call succeeded; a failed
88916
+ * call never delivered its content to the model, so it is dropped. Both
88917
+ * outcomes clear the pending entry, so the map cannot grow unbounded.
88918
+ */
88919
+ settlePrivateRead(toolCallId, failed) {
88920
+ const description = this.localPendingPrivateReads.get(toolCallId);
88921
+ if (description === void 0) return;
88922
+ this.localPendingPrivateReads.delete(toolCallId);
88923
+ if (failed) return;
88924
+ this.localTaintedPrivateReads.add(description);
88925
+ }
88926
+ /**
88927
+ * First half of two-phase private-read tracking: a call that reads,
88928
+ * searches, or read-writes a path outside the workspace is recorded as a
88929
+ * pending candidate here. Promotion happens in `settlePrivateRead` once the
88930
+ * result is known — denied, blocked, and failed calls must not taint the
88931
+ * session. The description never carries file content, only the path.
88932
+ */
88933
+ trackPendingPrivateRead(context) {
88934
+ const cwd = this.agent.config.cwd;
88935
+ if (cwd.length === 0) return;
88936
+ const pathClass = this.agent.jian.pathClass();
88937
+ const accesses = context.execution.accesses;
88938
+ if (accesses === void 0 || accesses.some((access) => access.kind === "all")) {
88939
+ const path = outsidePathInShellCommand(context, cwd, pathClass, this.agent.jian.gethome());
88940
+ if (path !== void 0) this.localPendingPrivateReads.set(context.toolCall.id, formatPrivateReadDescription(path));
88941
+ return;
88942
+ }
88943
+ const privateReads = accesses.filter(isPrivateReadAccess);
88944
+ if (privateReads.length === 0) return;
88945
+ const access = privateReads.find((fileAccess) => !isWithinDirectory$1(fileAccess.path, cwd, pathClass));
88946
+ if (access === void 0) return;
88947
+ this.localPendingPrivateReads.set(context.toolCall.id, formatPrivateReadDescription(access.path));
88948
+ }
88550
88949
  async beforeToolCall(context) {
88950
+ this.trackPendingPrivateRead(context);
88551
88951
  const evaluation = await this.evaluatePolicies(context);
88552
88952
  if (evaluation === void 0) return void 0;
88553
- return this.permissionPolicyResolutionToPrepare(evaluation.result, context, evaluation.policyName);
88953
+ const prepared = await this.permissionPolicyResolutionToPrepare(evaluation.result, context, evaluation.policyName);
88954
+ if (prepared?.block === true || prepared?.syntheticResult !== void 0) this.localPendingPrivateReads.delete(context.toolCall.id);
88955
+ return prepared;
88554
88956
  }
88555
88957
  async requestToolApproval(context, result, policyName) {
88556
88958
  const { signal } = context;
@@ -88572,10 +88974,12 @@ var PermissionManager = class {
88572
88974
  ...sourceCapabilityMode !== "all" ? { sourceCapabilityMode } : {},
88573
88975
  sourceToolName: name
88574
88976
  };
88977
+ const requestSummary = this.agent.turn.getLastPromptSummary();
88575
88978
  let response;
88576
88979
  if (this.agent.rpc?.requestApproval) {
88577
88980
  const approvalId = `approval-${String(++this.nextApprovalId)}`;
88578
88981
  let timeoutHandle;
88982
+ let timedOut = false;
88579
88983
  try {
88580
88984
  const customPromise = new Promise((resolve, reject) => {
88581
88985
  this.pendingApprovals.set(approvalId, {
@@ -88595,27 +88999,42 @@ var PermissionManager = class {
88595
88999
  toolName: name,
88596
89000
  action,
88597
89001
  display,
89002
+ reasons: result.reasons,
89003
+ grantOptions: result.grantOptions,
89004
+ ...requestSummary !== void 0 ? { requestSummary } : {},
88598
89005
  ...source
88599
89006
  }, { signal }).then(resolve, reject);
88600
89007
  });
88601
89008
  const timeoutPromise = new Promise((_, reject) => {
88602
89009
  timeoutHandle = setTimeout(() => {
89010
+ timedOut = true;
88603
89011
  reject(/* @__PURE__ */ new Error(`Approval request timed out after ${String(APPROVAL_TIMEOUT_MS)}ms`));
88604
89012
  }, APPROVAL_TIMEOUT_MS);
88605
89013
  });
88606
89014
  response = await Promise.race([customPromise, timeoutPromise]);
88607
89015
  } catch (error) {
88608
89016
  this.pendingApprovals.delete(approvalId);
89017
+ this.recordDecision(context, {
89018
+ decision: timedOut ? "timeout" : "cancelled",
89019
+ reason: errorText(error)
89020
+ });
88609
89021
  const resolved = result.resolveError?.(error);
88610
89022
  return await (resolved === void 0 ? Promise.reject(error) : this.permissionPolicyResolutionToPrepare(resolved, context, policyName));
88611
89023
  } finally {
88612
89024
  if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
88613
89025
  this.pendingApprovals.delete(approvalId);
88614
89026
  }
88615
- } else response = {
88616
- decision: "cancelled",
88617
- feedback: "Approval handler is unavailable."
88618
- };
89027
+ } else {
89028
+ response = {
89029
+ decision: "cancelled",
89030
+ feedback: "Approval handler is unavailable."
89031
+ };
89032
+ this.recordDecision(context, {
89033
+ decision: "cancelled",
89034
+ reason: "Approval handler is unavailable."
89035
+ });
89036
+ }
89037
+ response = this.downgradeUnofferedSessionScope(response, result.grantOptions, name);
88619
89038
  const sessionApprovalRule = response.decision === "approved" && response.scope === "session" ? context.execution.approvalRule : void 0;
88620
89039
  this.recordApprovalResult({
88621
89040
  turnId: Number(context.turnId),
@@ -88633,6 +89052,23 @@ var PermissionManager = class {
88633
89052
  reason: this.formatApprovalRejectionMessage(name, response)
88634
89053
  };
88635
89054
  }
89055
+ /**
89056
+ * Drop a session scope the policy never offered. Panels filter their buttons
89057
+ * by `grantOptions`, but the response is wire input — the core is the only
89058
+ * place that can guarantee a once-only prompt never memorizes a session
89059
+ * grant. Runs before the session rule is derived, so a downgraded response
89060
+ * leaves no `session-runtime` rule behind.
89061
+ */
89062
+ downgradeUnofferedSessionScope(response, grantOptions, toolName) {
89063
+ if (response.decision !== "approved" || response.scope !== "session") return response;
89064
+ if (grantOptions === void 0 || grantOptions.includes("session")) return response;
89065
+ this.agent.log.warn("Approval response requested a session grant that was not offered; downgrading to a one-time grant.", {
89066
+ toolName,
89067
+ grantOptions: [...grantOptions]
89068
+ });
89069
+ const { scope: _scope, ...rest } = response;
89070
+ return rest;
89071
+ }
88636
89072
  async evaluatePolicies(context) {
88637
89073
  for (const policy of this.policies) {
88638
89074
  const result = await policy.evaluate(context);
@@ -88645,13 +89081,37 @@ var PermissionManager = class {
88645
89081
  get effectiveRules() {
88646
89082
  return [...this.rules, ...this.parent?.effectiveRules ?? []];
88647
89083
  }
89084
+ /**
89085
+ * Append a stop decision to the wire, so a session's denials and aborted
89086
+ * approvals stay auditable. Uses the low-level record API on purpose: the
89087
+ * record is an audit trail, and replaying a session must not re-run a
89088
+ * decision (the live UI state already comes from the approval-result
89089
+ * records and the blocked tool's own error result).
89090
+ */
89091
+ recordDecision(context, decision) {
89092
+ this.agent.records.logRecord({
89093
+ type: "permission.record_decision",
89094
+ turnId: Number(context.turnId),
89095
+ toolCallId: context.toolCall.id,
89096
+ toolName: context.toolCall.name,
89097
+ ...decision
89098
+ });
89099
+ }
88648
89100
  permissionPolicyResolutionToPrepare(result, context, policyName) {
88649
89101
  switch (result.kind) {
88650
89102
  case "approve": return result.executionMetadata === void 0 ? void 0 : { executionMetadata: result.executionMetadata };
88651
- case "deny": return {
88652
- block: true,
88653
- reason: result.message ?? (typeof result.reason === "object" && result.reason !== null && typeof result.reason.reason === "string" ? `Tool "${context.toolCall.name}" was denied by permission policy: ${result.reason.reason}` : this.formatPolicyDenyMessage(context.toolCall.name))
88654
- };
89103
+ case "deny": {
89104
+ const reason = result.message ?? (typeof result.reason === "object" && result.reason !== null && typeof result.reason.reason === "string" ? `Tool "${context.toolCall.name}" was denied by permission policy: ${result.reason.reason}` : this.formatPolicyDenyMessage(context.toolCall.name));
89105
+ this.recordDecision(context, {
89106
+ ...policyName !== void 0 ? { policyName } : {},
89107
+ decision: "deny",
89108
+ reason
89109
+ });
89110
+ return {
89111
+ block: true,
89112
+ reason
89113
+ };
89114
+ }
88655
89115
  case "ask": return this.requestToolApproval(context, result, policyName);
88656
89116
  case "result": {
88657
89117
  const { kind: _kind, ...prepareResult } = result;
@@ -88672,6 +89132,96 @@ var PermissionManager = class {
88672
89132
  return prefix;
88673
89133
  }
88674
89134
  };
89135
+ function errorText(error) {
89136
+ return error instanceof Error ? error.message : String(error);
89137
+ }
89138
+ /** Operations that pull outside-workspace data into the session. */
89139
+ const PRIVATE_READ_OPERATIONS = new Set([
89140
+ "read",
89141
+ "search",
89142
+ "readwrite"
89143
+ ]);
89144
+ function isPrivateReadAccess(access) {
89145
+ return access.kind === "file" && PRIVATE_READ_OPERATIONS.has(access.operation);
89146
+ }
89147
+ /** Longest path kept in a taint description; the tail is what identifies the file. */
89148
+ const PRIVATE_READ_PATH_LIMIT = 120;
89149
+ function formatPrivateReadDescription(path) {
89150
+ return `read outside the workspace: ${path.length > PRIVATE_READ_PATH_LIMIT ? `…${path.slice(-120)}` : path}`;
89151
+ }
89152
+ /** Absolute paths, `~/…` and `$HOME/…` references in shell command text. */
89153
+ const SHELL_PATH_REFERENCE = /(?<!:)(?:~|\$\{?HOME\}?)?\/[^\s'"`;|&<>()]*/g;
89154
+ /** URLs are stripped before the scan: their host part is not a file path. */
89155
+ const SHELL_URL_REFERENCE = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`]+/gi;
89156
+ /**
89157
+ * Read-class commands: only these pull file *content* into the session. A
89158
+ * command that merely names a path — `ls /tmp`, `echo /etc`, a redirect
89159
+ * target, an env assignment — must not taint it, or ordinary build/test
89160
+ * commands would leave a permanent taint (and a permanent bot-mode denial).
89161
+ */
89162
+ const SHELL_READ_VERB = /\b(cat|head|tail|grep|rg|less|more|xxd|od|strings|base64|awk|sed|sort|uniq|cut)\b/;
89163
+ /** Tokens that introduce a write target. */
89164
+ const SHELL_WRITE_OPERATORS = new Set([
89165
+ ">",
89166
+ ">>",
89167
+ "-o",
89168
+ "-O",
89169
+ "--output",
89170
+ "tee"
89171
+ ]);
89172
+ /** Environment assignment (`PATH=…`), never a read. */
89173
+ const SHELL_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
89174
+ /** Glued redirects: `>/tmp/log` and fd forms like `2>/tmp/err`. */
89175
+ const SHELL_REDIRECT_PREFIX = /^\d*>>?/;
89176
+ /**
89177
+ * `github.com/foo` — a bare host followed by a path, as `curl`/`scp` accept
89178
+ * it. The leading segment makes it look like an absolute path to the scanner,
89179
+ * but it names a remote resource, not a local file.
89180
+ */
89181
+ const SHELL_HOST_SHAPED_TOKEN = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+\//;
89182
+ /**
89183
+ * First path reference in a shell command that resolves outside the workspace
89184
+ * (home expanded), or undefined.
89185
+ *
89186
+ * The scan only runs for read-class or egress commands and skips write targets,
89187
+ * assignments, and host-shaped tokens, because the guard exists to catch file
89188
+ * content entering the session — not every mention of a path. Known residues,
89189
+ * registered with the egress policy's blind-spot note: relative paths
89190
+ * (`cd / && cat .env`); readers or interpreters outside the verb list
89191
+ * (`python3 -c`, `perl -ne`, `dd`), which never taint through this scan; write
89192
+ * flag families that name a file without a redirect (`ssh-keygen -f`,
89193
+ * `openssl -i`) and commands such as `ssh-copy-id` that are not judged at all;
89194
+ * and paths hidden behind quoting or encoding.
89195
+ */
89196
+ function outsidePathInShellCommand(context, cwd, pathClass, homeDir) {
89197
+ if (context.toolCall.name !== "Bash") return void 0;
89198
+ const command = context.args?.command;
89199
+ if (typeof command !== "string") return void 0;
89200
+ if (!EGRESS_COMMAND_PATTERN.test(command) && !SHELL_READ_VERB.test(command)) return void 0;
89201
+ const tokens = command.replace(SHELL_URL_REFERENCE, " ").split(/\s+/);
89202
+ for (let index = 0; index < tokens.length; index += 1) {
89203
+ const token = tokens[index] ?? "";
89204
+ if (isShellWriteTarget(token, tokens[index - 1])) continue;
89205
+ if (SHELL_HOST_SHAPED_TOKEN.test(token)) continue;
89206
+ for (const match of token.matchAll(SHELL_PATH_REFERENCE)) {
89207
+ const path = expandHomeReference(match[0], homeDir);
89208
+ if (!isWithinDirectory$1(path, cwd, pathClass)) return path;
89209
+ }
89210
+ }
89211
+ }
89212
+ function isShellWriteTarget(token, previous) {
89213
+ if (token.length === 0) return false;
89214
+ if (SHELL_ASSIGNMENT.test(token)) return true;
89215
+ if (SHELL_REDIRECT_PREFIX.test(token)) return true;
89216
+ if (SHELL_WRITE_OPERATORS.has(token)) return true;
89217
+ return previous !== void 0 && SHELL_WRITE_OPERATORS.has(previous);
89218
+ }
89219
+ function expandHomeReference(path, homeDir) {
89220
+ if (path.startsWith("~/")) return `${homeDir}${path.slice(1)}`;
89221
+ if (path.startsWith("${HOME}/")) return `${homeDir}${path.slice(7)}`;
89222
+ if (path.startsWith("$HOME/")) return `${homeDir}${path.slice(5)}`;
89223
+ return path;
89224
+ }
88675
89225
  //#endregion
88676
89226
  //#region ../../packages/agent-core/src/utils/hero-slug.ts
88677
89227
  /** Hero-name slug generator: produces "hero-hero-hero" with collision fallback. */
@@ -89767,16 +90317,30 @@ const MIGRATIONS = [
89767
90317
  migrateRecord(record) {
89768
90318
  return record;
89769
90319
  }
90320
+ },
90321
+ {
90322
+ sourceVersion: "1.6",
90323
+ targetVersion: "1.7",
90324
+ migrateRecord(record) {
90325
+ return record;
90326
+ }
90327
+ },
90328
+ {
90329
+ sourceVersion: "1.7",
90330
+ targetVersion: "1.8",
90331
+ migrateRecord(record) {
90332
+ return record;
90333
+ }
89770
90334
  }
89771
90335
  ];
89772
90336
  function isNewerWireVersion(readVersion) {
89773
- return compareWireVersions(readVersion, "1.6") > 0;
90337
+ return compareWireVersions(readVersion, "1.8") > 0;
89774
90338
  }
89775
90339
  function resolveWireMigrations(readVersion) {
89776
- if (compareWireVersions(readVersion, "1.6") >= 0) return [];
90340
+ if (compareWireVersions(readVersion, "1.8") >= 0) return [];
89777
90341
  const migrations = [];
89778
90342
  let version = readVersion;
89779
- while (compareWireVersions(version, "1.6") < 0) {
90343
+ while (compareWireVersions(version, "1.8") < 0) {
89780
90344
  const migration = findMigration(version);
89781
90345
  if (migration === void 0) throw new Error(`Missing wire migration for version ${version}`);
89782
90346
  migrations.push(migration);
@@ -89884,7 +90448,7 @@ function headStartsWithAnyPrefix(head, headLength, prefixes) {
89884
90448
  */
89885
90449
  function readSkipFoldedFlag(headerText) {
89886
90450
  try {
89887
- return JSON.parse(headerText).protocol_version === "1.6";
90451
+ return JSON.parse(headerText).protocol_version === "1.8";
89888
90452
  } catch {
89889
90453
  return false;
89890
90454
  }
@@ -90623,6 +91187,10 @@ function restoreAgentRecord(agent, input) {
90623
91187
  case "permission.record_approval_result":
90624
91188
  agent.permission.recordApprovalResult(input);
90625
91189
  return;
91190
+ case "permission.record_grant_revocation":
91191
+ agent.permission.revokeSessionGrant(input.pattern);
91192
+ return;
91193
+ case "permission.record_decision": return;
90626
91194
  case "usage.record":
90627
91195
  agent.usage.record(input.model, input.usage, input.usageScope ?? "session", { skipCurrentTurn: true });
90628
91196
  return;
@@ -90743,7 +91311,7 @@ var AgentRecords = class {
90743
91311
  if (this.persistence !== void 0 && !this.metadataInitialized && stamped.type !== "metadata") {
90744
91312
  this.persistence.append({
90745
91313
  type: "metadata",
90746
- protocol_version: "1.6",
91314
+ protocol_version: "1.8",
90747
91315
  created_at: Date.now()
90748
91316
  });
90749
91317
  this.metadataInitialized = true;
@@ -90774,18 +91342,18 @@ var AgentRecords = class {
90774
91342
  this.metadataInitialized = true;
90775
91343
  const readVersion = record.protocol_version;
90776
91344
  if (isNewerWireVersion(readVersion)) {
90777
- warning = `Session wire protocol version ${readVersion} is newer than the current version 1.6. Records will be replayed without migration.`;
91345
+ warning = `Session wire protocol version ${readVersion} is newer than the current version 1.8. Records will be replayed without migration.`;
90778
91346
  shouldRewrite = false;
90779
91347
  } else {
90780
91348
  migrations = resolveWireMigrations(readVersion);
90781
- shouldRewrite = readVersion !== "1.6";
91349
+ shouldRewrite = readVersion !== "1.8";
90782
91350
  }
90783
91351
  buffered = shouldRewrite || warning !== void 0;
90784
91352
  }
90785
91353
  let migratedRecord = migrateWireRecord(record, migrations);
90786
91354
  if (migratedRecord.type === "metadata") migratedRecord = {
90787
91355
  ...migratedRecord,
90788
- protocol_version: "1.6"
91356
+ protocol_version: "1.8"
90789
91357
  };
90790
91358
  if (buffered) {
90791
91359
  replayedRecords.push(migratedRecord);
@@ -103281,6 +103849,11 @@ const SCREAM_MCP_CLIENT_VERSION = getCoreVersion();
103281
103849
  * including either the configured tool-call timeout, an in-flight abort
103282
103850
  * signal, both, or neither. Returns `undefined` when nothing needs to be
103283
103851
  * passed so the SDK falls back to its defaults.
103852
+ *
103853
+ * `McpServerConfig.toolTimeoutMs` resolves to `DEFAULT_TOOL_TIMEOUT_MS` when
103854
+ * unset, which equals the SDK's own request timeout — a config-derived call
103855
+ * therefore arrives here with the same budget the SDK would have applied on
103856
+ * its own.
103284
103857
  */
103285
103858
  function buildRequestOptions(toolCallTimeoutMs, signal) {
103286
103859
  if (toolCallTimeoutMs === void 0 && signal === void 0) return void 0;
@@ -105262,10 +105835,21 @@ var TurnFlow = class {
105262
105835
  turnStartWorkingSetPathCount = 0;
105263
105836
  turnStartVerificationCount = 0;
105264
105837
  verificationFailureInjected = false;
105838
+ /**
105839
+ * Plain-text summary of the most recent prompt, carried onto approval
105840
+ * requests so the panel can attribute a prompt to what the user asked for.
105841
+ * In-memory only: it is UI context, not session state.
105842
+ */
105843
+ lastPromptSummary;
105265
105844
  constructor(agent) {
105266
105845
  this.agent = agent;
105267
105846
  }
105847
+ /** See {@link lastPromptSummary}. Undefined until a text prompt arrives. */
105848
+ getLastPromptSummary() {
105849
+ return this.lastPromptSummary;
105850
+ }
105268
105851
  prompt(input, origin = USER_PROMPT_ORIGIN) {
105852
+ this.lastPromptSummary = summarizePrompt(input);
105269
105853
  this.agent.records.logRecord({
105270
105854
  type: "turn.prompt",
105271
105855
  input,
@@ -105795,6 +106379,7 @@ var TurnFlow = class {
105795
106379
  finalizeToolResult: async (ctx) => {
105796
106380
  const finalResult = ctx.nested === true ? ctx.result : await deduper.finalizeResult(ctx.toolCall.id, ctx.toolCall.name, ctx.args, ctx.result);
105797
106381
  const { isError, output } = finalResult;
106382
+ this.agent.permission.settlePrivateRead(ctx.toolCall.id, isError === true);
105798
106383
  this.agent.sessionMemory.recordToolExecution(ctx.toolCall.name, summarizeToolArgs(ctx.args), isError === true, ctx.stepNumber);
105799
106384
  this.recordWorkingSetPaths(ctx.toolCall.name, ctx.args, Number(ctx.turnId));
105800
106385
  if (ctx.toolCall.name === "Bash" && typeof ctx.args.command === "string") {
@@ -105924,6 +106509,18 @@ var TurnFlow = class {
105924
106509
  return false;
105925
106510
  }
105926
106511
  };
106512
+ /** Longest prompt summary kept for the approval card. */
106513
+ const PROMPT_SUMMARY_LIMIT = 120;
106514
+ /**
106515
+ * Plain text of a prompt, whitespace-collapsed and truncated. Returns
106516
+ * undefined for input without text (attachments only), so consumers can omit
106517
+ * the field instead of rendering an empty row.
106518
+ */
106519
+ function summarizePrompt(input) {
106520
+ const text = input.filter((part) => part.type === "text").map((part) => part.text).join(" ").replaceAll(/\s+/g, " ").trim();
106521
+ if (text.length === 0) return void 0;
106522
+ return text.length > PROMPT_SUMMARY_LIMIT ? `${text.slice(0, PROMPT_SUMMARY_LIMIT)}…` : text;
106523
+ }
105927
106524
  //#endregion
105928
106525
  //#region ../../packages/agent-core/src/utils/completion-budget.ts
105929
106526
  const MIN_FLOOR = 1;
@@ -107442,6 +108039,7 @@ function transformPermissionData(data) {
107442
108039
  appendPermissionRules(rules, raw["allow"], "allow");
107443
108040
  appendPermissionRules(rules, raw["ask"], "ask");
107444
108041
  if (rules.length > 0) out["rules"] = rules;
108042
+ if (raw["egressAllowlist"] !== void 0) out["egressAllowlist"] = raw["egressAllowlist"];
107445
108043
  return out;
107446
108044
  }
107447
108045
  function appendPermissionRules(target, value, decision) {
@@ -107561,6 +108159,8 @@ function permissionToToml(permission, rawPermission) {
107561
108159
  delete out["ask"];
107562
108160
  if (permission.rules !== void 0) out["rules"] = permission.rules.map(permissionRuleToToml);
107563
108161
  else delete out["rules"];
108162
+ if (permission.egressAllowlist !== void 0) out["egress_allowlist"] = [...permission.egressAllowlist];
108163
+ else delete out["egress_allowlist"];
107564
108164
  return out;
107565
108165
  }
107566
108166
  function permissionRuleToToml(rule) {
@@ -110863,6 +111463,10 @@ function assertMcpInputSchema(toolName, inputSchema) {
110863
111463
  //#endregion
110864
111464
  //#region ../../packages/agent-core/src/mcp/connection-manager.ts
110865
111465
  const DEFAULT_STARTUP_TIMEOUT_MS = 6e4;
111466
+ /** Effective tool-call budget for a server: configured value or the default. */
111467
+ function resolveToolCallTimeoutMs(config) {
111468
+ return config.toolTimeoutMs ?? 6e4;
111469
+ }
110866
111470
  /**
110867
111471
  * Owns the lifecycle of every configured MCP server for a Session.
110868
111472
  *
@@ -111088,7 +111692,7 @@ var McpConnectionManager = class {
111088
111692
  return entry.attemptId;
111089
111693
  }
111090
111694
  createClient(config, name) {
111091
- const toolCallTimeoutMs = config.toolTimeoutMs;
111695
+ const toolCallTimeoutMs = resolveToolCallTimeoutMs(config);
111092
111696
  if (config.transport === "stdio") return new StdioMcpClient(config, { toolCallTimeoutMs });
111093
111697
  return new HttpMcpClient(config, {
111094
111698
  toolCallTimeoutMs,
@@ -112628,6 +113232,22 @@ var Session$1 = class {
112628
113232
  return this.skills.listSkills().map(summarizeSkill);
112629
113233
  }
112630
113234
  /**
113235
+ * Approval patterns with an active approve-for-session grant in this
113236
+ * session, in insertion order. Empty when the main agent has not been
113237
+ * instantiated (or has no grants).
113238
+ */
113239
+ getSessionApprovalGrants() {
113240
+ return this.agents.get("main")?.permission.sessionApprovalRulePatterns.slice() ?? [];
113241
+ }
113242
+ /**
113243
+ * Revoke one approve-for-session grant held by the main agent. Returns true
113244
+ * when a grant was removed; false when the pattern was not granted (or the
113245
+ * main agent does not exist yet).
113246
+ */
113247
+ revokeSessionApprovalGrant(pattern) {
113248
+ return this.agents.get("main")?.permission.revokeSessionGrant(pattern) ?? false;
113249
+ }
113250
+ /**
112631
113251
  * Dynamically load additional skill roots into the running session.
112632
113252
  * Used after a plugin is installed so its skills become available
112633
113253
  * without requiring the user to create a new session.
@@ -127257,7 +127877,7 @@ var OpenAlexPaperSource = class {
127257
127877
  };
127258
127878
  //#endregion
127259
127879
  //#region ../../packages/agent-core/src/session/export/manifest.ts
127260
- const WIRE_PROTOCOL_VERSION = "1.6";
127880
+ const WIRE_PROTOCOL_VERSION = "1.8";
127261
127881
  function buildExportManifest(args) {
127262
127882
  return {
127263
127883
  sessionId: args.summary.id,
@@ -128628,6 +129248,12 @@ var SessionAPIImpl = class {
128628
129248
  async removeSkill(payload) {
128629
129249
  return this.session.removeSkill(payload.skillName);
128630
129250
  }
129251
+ getSessionApprovalGrants(_payload) {
129252
+ return this.session.getSessionApprovalGrants();
129253
+ }
129254
+ revokeSessionApprovalGrant(payload) {
129255
+ return this.session.revokeSessionApprovalGrant(payload.pattern);
129256
+ }
128631
129257
  listMcpServers(_payload) {
128632
129258
  return this.session.mcp.list();
128633
129259
  }
@@ -129852,6 +130478,12 @@ var ScreamCore = class {
129852
130478
  listMcpServers({ sessionId, ...payload }) {
129853
130479
  return this.sessionApi(sessionId).listMcpServers(payload);
129854
130480
  }
130481
+ getSessionApprovalGrants({ sessionId, ...payload }) {
130482
+ return this.sessionApi(sessionId).getSessionApprovalGrants(payload);
130483
+ }
130484
+ revokeSessionApprovalGrant({ sessionId, ...payload }) {
130485
+ return this.sessionApi(sessionId).revokeSessionApprovalGrant(payload);
130486
+ }
129855
130487
  getMcpStartupMetrics({ sessionId, ...payload }) {
129856
130488
  return this.sessionApi(sessionId).getMcpStartupMetrics(payload);
129857
130489
  }
@@ -130777,6 +131409,15 @@ var SDKRpcClient = class {
130777
131409
  skillName: input.skillName
130778
131410
  });
130779
131411
  }
131412
+ async getSessionApprovalGrants(input) {
131413
+ return (await this.getRpc()).getSessionApprovalGrants({ sessionId: input.sessionId });
131414
+ }
131415
+ async revokeSessionApprovalGrant(input) {
131416
+ return (await this.getRpc()).revokeSessionApprovalGrant({
131417
+ sessionId: input.sessionId,
131418
+ pattern: input.pattern
131419
+ });
131420
+ }
130780
131421
  /**
130781
131422
  * Background tasks are registered per agent: a subagent's Bash/Agent task
130782
131423
  * lives in that subagent's own registry, so `agentId` selects whose registry
@@ -131342,6 +131983,19 @@ var Session = class {
131342
131983
  this.ensureOpen();
131343
131984
  return this.rpc.listMcpServers({ sessionId: this.id });
131344
131985
  }
131986
+ /** Approval patterns with an active approve-for-session grant. */
131987
+ async getSessionApprovalGrants() {
131988
+ this.ensureOpen();
131989
+ return this.rpc.getSessionApprovalGrants({ sessionId: this.id });
131990
+ }
131991
+ /** Revoke one approve-for-session grant; true when a grant was removed. */
131992
+ async revokeSessionApprovalGrant(pattern) {
131993
+ this.ensureOpen();
131994
+ return this.rpc.revokeSessionApprovalGrant({
131995
+ sessionId: this.id,
131996
+ pattern
131997
+ });
131998
+ }
131345
131999
  async getMcpStartupMetrics() {
131346
132000
  this.ensureOpen();
131347
132001
  return this.rpc.getMcpStartupMetrics({ sessionId: this.id });
@@ -132724,6 +133378,13 @@ const BUILTIN_SLASH_COMMANDS = [
132724
133378
  priority: 188,
132725
133379
  availability: "always"
132726
133380
  },
133381
+ {
133382
+ name: "grants",
133383
+ aliases: [],
133384
+ description: "registry.grants_desc",
133385
+ priority: 188,
133386
+ availability: "always"
133387
+ },
132727
133388
  {
132728
133389
  name: "editor",
132729
133390
  aliases: [],
@@ -134658,6 +135319,17 @@ function buildTraceCells({ wirePath }) {
134658
135319
  }, time);
134659
135320
  break;
134660
135321
  }
135322
+ case "permission.record_decision": {
135323
+ const decision = asString(record["decision"]) ?? "";
135324
+ const toolName = asString(record["toolName"]) ?? "";
135325
+ const policyName = asString(record["policyName"]);
135326
+ const reason = asString(record["reason"]);
135327
+ pushCell("context", `${decision === "timeout" ? "审批超时" : decision === "cancelled" ? "审批取消" : "审批拒绝"}: ${toolName}${policyName !== void 0 ? ` (${policyName})` : ""}`, {
135328
+ inputDetail: reason,
135329
+ sourceSeq: seq
135330
+ }, time);
135331
+ break;
135332
+ }
134661
135333
  case "request.header": {
134662
135334
  const provider = asString(record["provider"]) ?? "";
134663
135335
  const model = asString(record["model"]) ?? "";
@@ -139300,7 +139972,7 @@ async function guidedGoalSetup(host) {
139300
139972
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
139301
139973
  return;
139302
139974
  }
139303
- const { TextInputDialogComponent } = await import("./text-input-dialog-b-TqTP2z.mjs");
139975
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DCMeCMoC.mjs");
139304
139976
  const initialDesc = await promptText(host, TextInputDialogComponent, {
139305
139977
  title: t("goal.setup_title_initial"),
139306
139978
  subtitle: t("goal.setup_desc_hint"),
@@ -139321,7 +139993,7 @@ async function guidedGoalSetup(host) {
139321
139993
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
139322
139994
  }
139323
139995
  async function showGoalConfigWizard(host, session, objective, replace) {
139324
- const { TextInputDialogComponent } = await import("./text-input-dialog-b-TqTP2z.mjs");
139996
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DCMeCMoC.mjs");
139325
139997
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
139326
139998
  title: t("goal.wizard_title", { objective }),
139327
139999
  subtitle: t("goal.budget_turns_hint"),
@@ -150747,6 +151419,53 @@ async function handleKnowledgeCommand(host, _args) {
150747
151419
  showMenu();
150748
151420
  }
150749
151421
  //#endregion
151422
+ //#region src/tui/commands/grants.ts
151423
+ async function handleGrantsCommand(host) {
151424
+ const session = host.session;
151425
+ if (session === void 0) {
151426
+ host.showError(getNoActiveSessionMessage());
151427
+ return;
151428
+ }
151429
+ let grants;
151430
+ try {
151431
+ grants = await session.getSessionApprovalGrants();
151432
+ } catch (error) {
151433
+ host.showError(t("grants.load_failed", { msg: formatErrorMessage(error) }));
151434
+ return;
151435
+ }
151436
+ if (grants.length === 0) {
151437
+ host.showStatus(t("grants.empty"), host.state.theme.colors.textDim);
151438
+ return;
151439
+ }
151440
+ host.mountEditorReplacement(new ChoicePickerComponent({
151441
+ title: t("grants.title", { count: String(grants.length) }),
151442
+ hint: t("grants.hint"),
151443
+ colors: host.state.theme.colors,
151444
+ options: grants.map((pattern) => ({
151445
+ value: pattern,
151446
+ label: pattern,
151447
+ tone: "danger"
151448
+ })),
151449
+ onSelect: (pattern) => {
151450
+ revokeGrant(host, session, pattern);
151451
+ },
151452
+ onCancel: () => {
151453
+ host.restoreEditor();
151454
+ }
151455
+ }));
151456
+ }
151457
+ async function revokeGrant(host, session, pattern) {
151458
+ try {
151459
+ const revoked = await session.revokeSessionApprovalGrant(pattern);
151460
+ host.restoreEditor();
151461
+ if (revoked) host.showStatus(t("grants.revoked", { pattern }), host.state.theme.colors.success);
151462
+ else host.showError(t("grants.revoke_failed", { pattern }));
151463
+ } catch {
151464
+ host.restoreEditor();
151465
+ host.showError(t("grants.revoke_failed", { pattern }));
151466
+ }
151467
+ }
151468
+ //#endregion
150750
151469
  //#region ../../packages/evals/src/judge.ts
150751
151470
  /** Runs every rule against the input; a rule passes when its check is true. */
150752
151471
  function judgeOutput(input, rules) {
@@ -151287,6 +152006,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
151287
152006
  case "permission":
151288
152007
  showPermissionPicker(host);
151289
152008
  return;
152009
+ case "grants":
152010
+ await handleGrantsCommand(host);
152011
+ return;
151290
152012
  case "settings":
151291
152013
  showSettingsSelector(host);
151292
152014
  return;
@@ -151405,4 +152127,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
151405
152127
  }
151406
152128
  }
151407
152129
  //#endregion
151408
- export { handleExportDebugZipCommand as $, encodeWorkDirKey as $n, lerpGradient as $t, highlightLines as A, embeddingFailureDetail as An, isTurnElapsedEnabled as At, ActivityGroupComponent as B, detectShellEnvironment as Bn, ENABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getLlmNotSetMessage as Cn, clearInfoPanelState as Ct, ToolCallComponent as D, sortSlashCommands as Dn, createEditorTheme as Dt, UserMessageComponent as E, BUILTIN_SLASH_COMMANDS as En, resolveThemeSync as Et, StatusMessageComponent as F, PULSE_WAVE_FRAMES as Fn, detectTerminalTheme as Ft, AgentGroupComponent as G, CLI_COMMAND_NAME as Gn, QUERY_TERMINAL_THEME as Gt, estimateTokens as H, getInputHistoryFile as Hn, OSC11_RESPONSE as Ht, replaceTabs as I, TuiConfigParseError as In, parseOsc11BackgroundTheme as It, getBreathingFrame as J, PRODUCT_NAME as Jn, TERMINAL_THEME_DARK as Jt, WelcomeComponent as K, CLI_UI_MODE as Kn, TERMINAL_FOCUS_IN as Kt, sanitizeShellOutput as L, TuiLikePreferencesSchema as Ln, DISABLE_TERMINAL_FOCUS_REPORTING as Lt, CachedContainer as M, hasEmbeddingModelCache as Mn, createThemeStyles as Mt, ThinkingComponent as N, startManualEmbeddingDownload as Nn, contrastTextHex as Nt, renderDiffLines as O, isExperimentalFlagEnabled as On, createMarkdownTheme as Ot, NoticeMessageComponent as P, PIXEL_PULSE_FRAMES as Pn, getColorPalette as Pt, refineGoal as Q, ScreamHarness as Qn, FooterComponent as Qt, SkillActivationComponent as R, loadTuiConfig as Rn, DISABLE_TERMINAL_THEME_REPORTING as Rt, readJsonlFile as S, getCtrlDHint as Sn, supportsBalance as St, handleRevokeCommand as T, buildSkillSlashCommands as Tn, showUsage as Tt, getSharedSpeedTracker as U, getLogDir as Un, OSC11_RESPONSE_PREFIX as Ut, charsForTokenBudget as V, getDataDir as Vn, OSC11_QUERY as Vt, BackgroundAgentStatusComponent as W, detectInstallSource as Wn, OSC11_RESPONSE_PREFIX_NO_ESC as Wt, CompactionComponent as X, fetchCatalog as Xn, isBusy as Xt, resetBreathingClock as Y, DEFAULT_CATALOG_URL as Yn, TERMINAL_THEME_LIGHT as Yt, clearGoalState as Z, saveCatalogCache as Zn, isStreaming as Zt, handleUpdateCommand as _, EXIT_CONFIRM_WINDOW_MS as _n, handleYoloCommand as _t, syncDiagramCapabilityPrompt as a, printableChar as an, isRealUserPrompt as ar, toTerminalHyperlink as at, readUpdateCache as b, TIP_ROTATION_INTERVAL_MS as bn, showSettingsSelector as bt, disposeChildren as c, appendStreamingArgsPreview as cn, flushDiagnosticLogs as cr, handleAutoCommand as ct, isPlanExpandable as d, isTodoItemShape as dn, isScreamError as dr, handleEditorCommand as dt, SubagentSlots as en, normalizeWorkDir as er, handleExportMdCommand as et, MoonLoader as f, parseStreamingArgs as fn, isOrphanedToolCallError as fr, handleFusionPlanCommand as ft, handleMcpCommand as g, EMPTY_SESSION_HINT_URL as gn, handleWolfpackCommand as gt, handleChannelCommand as h, truncateErrorMessage as hn, handleThemeCommand as ht, buildRoleAdditionalText as i, handleLogoutCommand as in, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ir, isInternalMessage as it, langFromPath as j, getKnowledgeStore as jn, toggleEmptySessionHint as jt, renderDiffLinesClustered as k, setExperimentalFlags as kn, isEmptySessionHintDismissed as kt, hasDispose as l, argsRecord as ln, log as lr, handleBotCommand as lt, handleMemoryCommand as m, stringValue as mn, SCREAM_ERROR_INFO as mr, handlePlanCommand as mt, clearEvalPanelState as n, handleSearchCommand as nn, readSessionIndex as nr, handleInitCommand as nt, handleExtensionCommand as o, STATUS_BULLET as on, parseRlmMaxDepthArg as or, changeThinkingLevel as ot, formatMemoryMemoForInjection as p, serializeToolResultOutput as pn, ErrorCodes as pr, handleModelCommand as pt, BREATHE_CYCLE_MS as q, CLI_USER_AGENT_PRODUCT as qn, TERMINAL_FOCUS_OUT as qt, openUrl as r, handleConnectCommand as rn, removeSessionIndexEntry as rr, handleTitleCommand as rt, handleSkillCommand as s, appendSessionHeaderHint as sn, resolveScreamHome as sr, getModelCycleLevel as st, dispatchInput as t, handleTraceCommand as tn, appendSessionIndexEntry as tr, handleForkCommand as tt, isExpandable as u, formatErrorMessage as un, resolveGlobalLogPath as ur, handleCompactCommand as ut, selectUpdateTarget as v, MAIN_AGENT_ID$1 as vn, showModelPicker as vt, getDaemonInstructions as w, getNoActiveSessionMessage as wn, showStatusReport as wt, appendJsonlLine as x, getCtrlCHint as xn, refreshProviderBalance as xt, refreshUpdateCache as y, SESSION_TIPS as yn, showPermissionPicker as yt, AssistantMessageComponent as z, saveTuiConfig as zn, ENABLE_TERMINAL_FOCUS_REPORTING as zt };
152130
+ export { refineGoal as $, ScreamHarness as $n, FooterComponent as $t, renderDiffLinesClustered as A, setExperimentalFlags as An, isEmptySessionHintDismissed as At, AssistantMessageComponent as B, saveTuiConfig as Bn, ENABLE_TERMINAL_FOCUS_REPORTING as Bt, readJsonlFile as C, getCtrlDHint as Cn, supportsBalance as Ct, UserMessageComponent as D, BUILTIN_SLASH_COMMANDS as Dn, resolveThemeSync as Dt, handleRevokeCommand as E, buildSkillSlashCommands as En, showUsage as Et, NoticeMessageComponent as F, PIXEL_PULSE_FRAMES as Fn, getColorPalette as Ft, BackgroundAgentStatusComponent as G, detectInstallSource as Gn, OSC11_RESPONSE_PREFIX_NO_ESC as Gt, charsForTokenBudget as H, getDataDir as Hn, OSC11_QUERY as Ht, StatusMessageComponent as I, PULSE_WAVE_FRAMES as In, detectTerminalTheme as It, BREATHE_CYCLE_MS as J, CLI_USER_AGENT_PRODUCT as Jn, TERMINAL_FOCUS_OUT as Jt, AgentGroupComponent as K, CLI_COMMAND_NAME as Kn, QUERY_TERMINAL_THEME as Kt, replaceTabs as L, TuiConfigParseError as Ln, parseOsc11BackgroundTheme as Lt, langFromPath as M, getKnowledgeStore as Mn, toggleEmptySessionHint as Mt, CachedContainer as N, hasEmbeddingModelCache as Nn, createThemeStyles as Nt, ToolCallComponent as O, sortSlashCommands as On, createEditorTheme as Ot, ThinkingComponent as P, startManualEmbeddingDownload as Pn, contrastTextHex as Pt, clearGoalState as Q, saveCatalogCache as Qn, isStreaming as Qt, sanitizeShellOutput as R, TuiLikePreferencesSchema as Rn, DISABLE_TERMINAL_FOCUS_REPORTING as Rt, appendJsonlLine as S, getCtrlCHint as Sn, refreshProviderBalance as St, getDaemonInstructions as T, getNoActiveSessionMessage as Tn, showStatusReport as Tt, estimateTokens as U, getInputHistoryFile as Un, OSC11_RESPONSE as Ut, ActivityGroupComponent as V, detectShellEnvironment as Vn, ENABLE_TERMINAL_THEME_REPORTING as Vt, getSharedSpeedTracker as W, getLogDir as Wn, OSC11_RESPONSE_PREFIX as Wt, resetBreathingClock as X, DEFAULT_CATALOG_URL as Xn, TERMINAL_THEME_LIGHT as Xt, getBreathingFrame as Y, PRODUCT_NAME as Yn, TERMINAL_THEME_DARK as Yt, CompactionComponent as Z, fetchCatalog as Zn, isBusy as Zt, handleMcpCommand as _, EMPTY_SESSION_HINT_URL as _n, handleWolfpackCommand as _t, buildRoleAdditionalText as a, handleLogoutCommand as an, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ar, isInternalMessage as at, refreshUpdateCache as b, SESSION_TIPS as bn, showPermissionPicker as bt, handleSkillCommand as c, appendSessionHeaderHint as cn, resolveScreamHome as cr, getModelCycleLevel as ct, isExpandable as d, formatErrorMessage as dn, resolveGlobalLogPath as dr, handleCompactCommand as dt, lerpGradient as en, encodeWorkDirKey as er, handleExportDebugZipCommand as et, isPlanExpandable as f, isTodoItemShape as fn, isScreamError as fr, handleEditorCommand as ft, handleChannelCommand as g, truncateErrorMessage as gn, handleThemeCommand as gt, handleMemoryCommand as h, stringValue as hn, SCREAM_ERROR_INFO as hr, handlePlanCommand as ht, openUrl as i, handleConnectCommand as in, removeSessionIndexEntry as ir, handleTitleCommand as it, highlightLines as j, embeddingFailureDetail as jn, isTurnElapsedEnabled as jt, renderDiffLines as k, isExperimentalFlagEnabled as kn, createMarkdownTheme as kt, disposeChildren as l, appendStreamingArgsPreview as ln, flushDiagnosticLogs as lr, handleAutoCommand as lt, formatMemoryMemoForInjection as m, serializeToolResultOutput as mn, ErrorCodes as mr, handleModelCommand as mt, clearEvalPanelState as n, handleTraceCommand as nn, appendSessionIndexEntry as nr, handleForkCommand as nt, syncDiagramCapabilityPrompt as o, printableChar as on, isRealUserPrompt as or, toTerminalHyperlink as ot, MoonLoader as p, parseStreamingArgs as pn, isOrphanedToolCallError as pr, handleFusionPlanCommand as pt, WelcomeComponent as q, CLI_UI_MODE as qn, TERMINAL_FOCUS_IN as qt, handleGrantsCommand as r, handleSearchCommand as rn, readSessionIndex as rr, handleInitCommand as rt, handleExtensionCommand as s, STATUS_BULLET as sn, parseRlmMaxDepthArg as sr, changeThinkingLevel as st, dispatchInput as t, SubagentSlots as tn, normalizeWorkDir as tr, handleExportMdCommand as tt, hasDispose as u, argsRecord as un, log as ur, handleBotCommand as ut, handleUpdateCommand as v, EXIT_CONFIRM_WINDOW_MS as vn, handleYoloCommand as vt, handleCcCommand as w, getLlmNotSetMessage as wn, clearInfoPanelState as wt, readUpdateCache as x, TIP_ROTATION_INTERVAL_MS as xn, showSettingsSelector as xt, selectUpdateTarget as y, MAIN_AGENT_ID$1 as yn, showModelPicker as yt, SkillActivationComponent as z, loadTuiConfig as zn, DISABLE_TERMINAL_THEME_REPORTING as zt };