@cjhyy/code-shell-core 0.8.8 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,7 +44,7 @@ import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
44
44
  import { detectPastedNoise } from "../utils/task-sanitizer.js";
45
45
  import { PromptCacheDiagnosticRecorder, promptCacheDropHint, } from "./prompt-cache-diagnostics.js";
46
46
  import { buildRunUserMessageContent, prepareRunImageInput } from "./run-image-input.js";
47
- import { QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
47
+ import { ISOLATED_TASK_PROFILE, QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
48
48
  import { createSubAgentSpawner } from "./subagent-spawner.js";
49
49
  import { AuxiliaryPipeline, sameLlmIdentity } from "./auxiliary-pipeline.js";
50
50
  import { PermissionController } from "./permission-controller.js";
@@ -413,6 +413,7 @@ export class Engine {
413
413
  // extension modules — later registrations override earlier ones by id.
414
414
  this.behaviorProfiles = new Map([
415
415
  QUICK_CHAT_RESTRICTED_PROFILE,
416
+ ISOLATED_TASK_PROFILE,
416
417
  ...(config.behaviorProfiles ?? []),
417
418
  ...(config.extensionModules ?? []).flatMap((module) => module.behaviorProfiles ?? []),
418
419
  ].map((profile) => [profile.id, profile]));
@@ -1099,16 +1100,18 @@ export class Engine {
1099
1100
  session = openedResult.opened.session;
1100
1101
  this.stampRunToolContext(toolCtx, session, options);
1101
1102
  const sessionRun = runWithSid(session.state.sessionId, async () => {
1102
- const hookMessages = await this.runSessionStartHooks({
1103
- session,
1104
- task,
1105
- cwd,
1106
- runPermissionMode,
1107
- resumedFromDisk,
1108
- options,
1109
- taskText,
1110
- messages,
1111
- });
1103
+ const hookMessages = profile?.disableHooks
1104
+ ? []
1105
+ : await this.runSessionStartHooks({
1106
+ session,
1107
+ task,
1108
+ cwd,
1109
+ runPermissionMode,
1110
+ resumedFromDisk,
1111
+ options,
1112
+ taskText,
1113
+ messages,
1114
+ });
1112
1115
  const sid = session.state.sessionId;
1113
1116
  const { contextManager, llmClientPromise, toolExecutor } = this.wireRunContextAndPermission({
1114
1117
  session,
@@ -1939,17 +1942,23 @@ export class Engine {
1939
1942
  // A profile whose tool allowlist excludes the Skill tool can never invoke
1940
1943
  // a skill, so the full skills listing would be dead context for every one
1941
1944
  // of its turns (e.g. the Pet manager) — inject none via an empty allowlist.
1942
- const profileCanUseSkills = !profile?.allowedToolNames || profile.allowedToolNames.has(skillToolDef.name);
1945
+ const runAllowedToolNames = toolCtx.allowedToolNames;
1946
+ const profileCanUseSkills = !runAllowedToolNames || runAllowedToolNames.has(skillToolDef.name);
1943
1947
  const promptComposer = new PromptComposer(buildPromptComposerConfig({
1944
1948
  cwd,
1945
1949
  model: this.config.llm.model,
1946
1950
  preset: this.preset,
1947
- customSystemPrompt: this.config.customSystemPrompt,
1948
- appendSystemPrompt: [this.config.appendSystemPrompt, profile?.systemPromptAppend]
1951
+ customSystemPrompt: profile?.disableInstructions
1952
+ ? undefined
1953
+ : this.config.customSystemPrompt,
1954
+ appendSystemPrompt: [
1955
+ profile?.disableInstructions ? undefined : this.config.appendSystemPrompt,
1956
+ profile?.systemPromptAppend,
1957
+ ]
1949
1958
  .filter(Boolean)
1950
1959
  .join("\n\n") || undefined,
1951
1960
  responseLanguage: this.config.responseLanguage,
1952
- userProfile: this.config.userProfile,
1961
+ userProfile: profile?.disableInstructions ? undefined : this.config.userProfile,
1953
1962
  workspaceProfile: runWorkspaceProfile,
1954
1963
  // Read from the Session's own persisted state, so every turn — not just
1955
1964
  // the first — carries the standing brief.
@@ -1959,15 +1968,21 @@ export class Engine {
1959
1968
  instructionBoundaryFinder: (scanCwd) => resolveInstructionBoundary(scanCwd, this.capabilities),
1960
1969
  disabledSkills,
1961
1970
  disabledPlugins,
1962
- skillAllowlist: profileCanUseSkills ? this.config.skillAllowlist : [],
1971
+ skillAllowlist: profileCanUseSkills ? toolCtx.skillAllowlist : [],
1963
1972
  memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
1964
1973
  memoryCurrentProjectOnly: profile?.memoryCurrentProjectOnly,
1965
- goalToolState: !profile?.allowedToolNames ||
1966
- profile.allowedToolNames.has("complete_goal") ||
1967
- profile.allowedToolNames.has("cancel_goal")
1974
+ disableInstructions: profile?.disableInstructions,
1975
+ disableMemoryContext: profile?.disableMemoryContext,
1976
+ disableCapabilityContext: profile?.disableCapabilityContext,
1977
+ disableSourcesContext: profile?.disableSourcesContext,
1978
+ goalToolState: !runAllowedToolNames ||
1979
+ runAllowedToolNames.has("complete_goal") ||
1980
+ runAllowedToolNames.has("cancel_goal")
1968
1981
  ? { hasGoal: hasRunnableGoal }
1969
1982
  : undefined,
1970
- capabilityPromptSections: this.capabilityPromptSections,
1983
+ capabilityPromptSections: profile?.disableCapabilityContext
1984
+ ? {}
1985
+ : this.capabilityPromptSections,
1971
1986
  dynamicContextProviders: this.capabilityDynamicContextProviders,
1972
1987
  getSettingsManager: () => this.getSettingsManager(),
1973
1988
  toolCatalog: this.toolCatalog,
@@ -2024,7 +2039,7 @@ export class Engine {
2024
2039
  toolRewriters: this.toolRewriters,
2025
2040
  toolFeatureFlags: TOOL_FEATURE_FLAGS,
2026
2041
  applyBuiltinOverrideVisibility,
2027
- profileAllowedToolNames: profile?.allowedToolNames,
2042
+ profileAllowedToolNames: runAllowedToolNames,
2028
2043
  runPlanMode,
2029
2044
  });
2030
2045
  return { promptComposer, toolDefs };
@@ -88,11 +88,13 @@ export async function finalizeRunSuccess(args) {
88
88
  // the turn loop has resolved (completion, error, or abort). Handlers
89
89
  // are notify-only — any returned messages are dropped because the run
90
90
  // is already over and there's no next turn to inject into.
91
- await args.emitHook("on_session_end", {
92
- sessionId: session.state.sessionId,
93
- reason: result.reason,
94
- turnCount,
95
- }, options?.signal);
91
+ if (profile?.disableHooks !== true) {
92
+ await args.emitHook("on_session_end", {
93
+ sessionId: session.state.sessionId,
94
+ reason: result.reason,
95
+ turnCount,
96
+ }, options?.signal);
97
+ }
96
98
  // Ephemeral side chats must never leak into durable memory, even after
97
99
  // the user explicitly elevates tool permissions for a turn. Lifecycle
98
100
  // isolation is independent of the run-scoped behavior/permission mode.
@@ -106,7 +108,7 @@ export async function finalizeRunSuccess(args) {
106
108
  // Reuses the already-resolved auxSummaryClient (aux model, cheap). Best-
107
109
  // effort: failures never touch the run result. The renderer writes the
108
110
  // title into the sidebar on receipt of the session_title stream event.
109
- {
111
+ if (profile?.disableSessionTitle !== true) {
110
112
  const messageEvents = session.transcript.getEvents("message");
111
113
  const userMsgEvents = messageEvents.filter((e) => e.data.role === "user");
112
114
  const userMsgCount = userMsgEvents.length;
@@ -113,7 +113,7 @@ export function openRunSession(args) {
113
113
  else {
114
114
  // Cold start: shape (2) reuses the host-supplied sid; shape (3)
115
115
  // lets sessionManager generate one with nanoid.
116
- session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind);
116
+ session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind, options?.ephemeral === true);
117
117
  const userMsg = { role: "user", content: args.userMessageContent };
118
118
  claimClientMessageId(session, options?.clientMessageId, "submit");
119
119
  if (args.parsedTask.hasImages)
@@ -33,6 +33,10 @@ export interface RunPromptComposerConfigInput {
33
33
  skillAllowlist: ComposerOptions["skillAllowlist"];
34
34
  memoriesMaxAgeDays: ComposerOptions["memoriesMaxAgeDays"];
35
35
  memoryCurrentProjectOnly?: ComposerOptions["memoryCurrentProjectOnly"];
36
+ disableInstructions?: ComposerOptions["disableInstructions"];
37
+ disableMemoryContext?: ComposerOptions["disableMemoryContext"];
38
+ disableCapabilityContext?: ComposerOptions["disableCapabilityContext"];
39
+ disableSourcesContext?: ComposerOptions["disableSourcesContext"];
36
40
  goalToolState: ComposerOptions["goalToolState"];
37
41
  capabilityPromptSections: ComposerOptions["capabilityPromptSections"];
38
42
  dynamicContextProviders: ComposerOptions["dynamicContextProviders"];
@@ -23,7 +23,7 @@ export function resolveRunProfileState(args) {
23
23
  }
24
24
  /** Build the prompt-composer options for a run without capturing the Engine facade. */
25
25
  export function buildPromptComposerConfig(args) {
26
- const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
26
+ const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
27
27
  return {
28
28
  cwd,
29
29
  model,
@@ -50,6 +50,10 @@ export function buildPromptComposerConfig(args) {
50
50
  skillAllowlist,
51
51
  memoriesMaxAgeDays,
52
52
  memoryCurrentProjectOnly,
53
+ disableInstructions,
54
+ disableMemoryContext,
55
+ disableCapabilityContext,
56
+ disableSourcesContext,
53
57
  goalToolState,
54
58
  capabilityPromptSections,
55
59
  dynamicContextProviders,
@@ -51,8 +51,14 @@ export function buildRunToolContext(args) {
51
51
  return reason;
52
52
  },
53
53
  },
54
+ skillAllowlist: options?.skillAllowlist !== undefined
55
+ ? [...options.skillAllowlist]
56
+ : args.base.skillAllowlist,
54
57
  };
55
- if (profile?.allowedToolNames) {
58
+ if (options?.toolAllowlist !== undefined) {
59
+ toolCtx.allowedToolNames = new Set(options.toolAllowlist);
60
+ }
61
+ else if (profile?.allowedToolNames) {
56
62
  toolCtx.allowedToolNames = profile.allowedToolNames;
57
63
  }
58
64
  if (profile?.createRunServices) {
@@ -32,6 +32,20 @@ export interface RunBehaviorProfile {
32
32
  disablePlanMode?: boolean;
33
33
  /** When true, MCP servers are neither connected nor exposed for the run. */
34
34
  disableMcp?: boolean;
35
+ /** Skip repository/user instruction discovery for this run. */
36
+ disableInstructions?: boolean;
37
+ /** Skip persistent-memory injection for this run. */
38
+ disableMemoryContext?: boolean;
39
+ /** Skip capability-owned volatile context providers for this run. */
40
+ disableCapabilityContext?: boolean;
41
+ /** Skip bound-source metadata injection for this run. */
42
+ disableSourcesContext?: boolean;
43
+ /** Ignore the project's default digital-human profile for this run. */
44
+ disableWorkspaceProfile?: boolean;
45
+ /** Skip SessionStart/UserPromptSubmit/SessionEnd hooks for this run. */
46
+ disableHooks?: boolean;
47
+ /** Skip auxiliary title generation for this run. */
48
+ disableSessionTitle?: boolean;
35
49
  /**
36
50
  * When true, the injected persistent-memory index is trimmed to this run's
37
51
  * project: global-layer project-type / dream-scope records tied to other
@@ -67,6 +81,10 @@ export interface RunBehaviorProfile {
67
81
  export declare const QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT = "# Side Conversation Boundary\n\nThis is a side conversation, not the main-thread task execution environment.\n- Treat all content before this boundary as reference history only. Do not proactively continue any earlier plan, task, or modification.\n- Default to answering the user's question directly. Use lightweight read-only exploration only when needed.\n- Do not modify files, git state, configuration, or permissions unless the user explicitly asks after this boundary (for example, \"Allow you to modify files, please help me...\" or \"Please directly edit...\"). When explicitly requested, use the normally available tools subject to the current permission and approval mode.\n- Sub-agents are disabled for this side conversation. Do not create or invoke sub-agents.";
68
82
  /** The side-conversation restriction expressed as a generic behavior profile. */
69
83
  export declare const QUICK_CHAT_RESTRICTED_PROFILE: RunBehaviorProfile;
84
+ export declare const ISOLATED_TASK_BEHAVIOR_MODE: "isolatedTask";
85
+ export declare const ISOLATED_TASK_SYSTEM_PROMPT = "# Isolated Task Boundary\n\nThis is a bounded Task, not a continuation of any user conversation.\n- Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.\n- Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.\n- Do not create sub-agents or start unrelated work.\n- Finish the requested outcome, then return a concise result suitable for the owning application.";
86
+ /** Minimal, process-local run profile used by host-owned Tasks. */
87
+ export declare const ISOLATED_TASK_PROFILE: RunBehaviorProfile;
70
88
  export interface EngineRunOptions {
71
89
  cwd?: string;
72
90
  onStream?: StreamCallback;
@@ -83,6 +101,12 @@ export interface EngineRunOptions {
83
101
  attachments?: InputAttachmentMeta[];
84
102
  /** Named per-run behavior profile supplied by interactive product surfaces. */
85
103
  behaviorMode?: RunBehaviorMode;
104
+ /** Per-run hard tool allowlist. An empty list exposes no tools. */
105
+ toolAllowlist?: readonly string[];
106
+ /** Per-run hard Skill allowlist. An empty list exposes no Skills. */
107
+ skillAllowlist?: readonly string[];
108
+ /** Keep a fresh Session in process memory only and omit it from Session pickers. */
109
+ ephemeral?: boolean;
86
110
  /**
87
111
  * Generic per-run parameters consumed by the active behavior profile
88
112
  * (createRunServices / buildVisibilityMeta / runtime-context injection).
@@ -10,3 +10,24 @@ export const QUICK_CHAT_RESTRICTED_PROFILE = {
10
10
  id: "quickChatRestricted",
11
11
  systemPromptAppend: QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT,
12
12
  };
13
+ export const ISOLATED_TASK_BEHAVIOR_MODE = "isolatedTask";
14
+ export const ISOLATED_TASK_SYSTEM_PROMPT = `# Isolated Task Boundary
15
+
16
+ This is a bounded Task, not a continuation of any user conversation.
17
+ - Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.
18
+ - Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.
19
+ - Do not create sub-agents or start unrelated work.
20
+ - Finish the requested outcome, then return a concise result suitable for the owning application.`;
21
+ /** Minimal, process-local run profile used by host-owned Tasks. */
22
+ export const ISOLATED_TASK_PROFILE = {
23
+ id: ISOLATED_TASK_BEHAVIOR_MODE,
24
+ systemPromptAppend: ISOLATED_TASK_SYSTEM_PROMPT,
25
+ disableMcp: true,
26
+ disableInstructions: true,
27
+ disableMemoryContext: true,
28
+ disableCapabilityContext: true,
29
+ disableSourcesContext: true,
30
+ disableWorkspaceProfile: true,
31
+ disableHooks: true,
32
+ disableSessionTitle: true,
33
+ };
@@ -93,11 +93,17 @@ export async function resolveRunWorkspace(args) {
93
93
  configCwd: args.configCwd,
94
94
  processCwd: args.processCwd,
95
95
  });
96
- const profileState = resolveRunProfileState({
97
- sessionWorkspaceProfile,
98
- cwd,
99
- settings: args.settings,
100
- });
96
+ const profileState = profile?.disableWorkspaceProfile
97
+ ? {
98
+ workspaceProfile: undefined,
99
+ sessionProfileOverrides: undefined,
100
+ profileMemoryDir: undefined,
101
+ }
102
+ : resolveRunProfileState({
103
+ sessionWorkspaceProfile,
104
+ cwd,
105
+ settings: args.settings,
106
+ });
101
107
  return {
102
108
  ok: true,
103
109
  resolution: {
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.8.8";
6
+ export declare const VERSION = "0.8.9";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
9
9
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.8.8";
6
+ export const VERSION = "0.8.9";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Engine (primary API) ────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export declare const PANEL_APP_MANIFEST_FILE = ".codeshell-panel/panel.json";
3
- export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"];
3
+ export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"];
4
4
  export declare const PANEL_APP_ICONS: readonly ["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"];
5
5
  export declare const PanelAppAgentTool: z.ZodEffects<z.ZodObject<{
6
6
  name: z.ZodString;
@@ -116,12 +116,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
116
116
  icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
117
117
  placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
118
118
  singleton: z.ZodDefault<z.ZodBoolean>;
119
- permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
119
+ permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"]>, "many">>;
120
120
  schemaVersion: z.ZodLiteral<1>;
121
121
  }, "strict", z.ZodTypeAny, {
122
122
  id: string;
123
123
  version: string;
124
- permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
124
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
125
125
  entry: string;
126
126
  title: {
127
127
  default: string;
@@ -144,7 +144,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
144
144
  };
145
145
  schemaVersion: 1;
146
146
  description?: string | undefined;
147
- permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
147
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
148
148
  icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
149
149
  placement?: "right-dock" | undefined;
150
150
  singleton?: boolean | undefined;
@@ -230,12 +230,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
230
230
  icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
231
231
  placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
232
232
  singleton: z.ZodDefault<z.ZodBoolean>;
233
- permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
233
+ permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "agent.task", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage", "process"]>, "many">>;
234
234
  schemaVersion: z.ZodLiteral<2>;
235
235
  }, "strict", z.ZodTypeAny, {
236
236
  id: string;
237
237
  version: string;
238
- permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
238
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
239
239
  entry: string;
240
240
  title: {
241
241
  default: string;
@@ -276,14 +276,14 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
276
276
  }[] | undefined;
277
277
  } | undefined;
278
278
  description?: string | undefined;
279
- permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
279
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
280
280
  icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
281
281
  placement?: "right-dock" | undefined;
282
282
  singleton?: boolean | undefined;
283
283
  }>]>, {
284
284
  id: string;
285
285
  version: string;
286
- permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
286
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
287
287
  entry: string;
288
288
  title: {
289
289
  default: string;
@@ -298,7 +298,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
298
298
  } | {
299
299
  id: string;
300
300
  version: string;
301
- permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
301
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[];
302
302
  entry: string;
303
303
  title: {
304
304
  default: string;
@@ -330,7 +330,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
330
330
  };
331
331
  schemaVersion: 1;
332
332
  description?: string | undefined;
333
- permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
333
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
334
334
  icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
335
335
  placement?: "right-dock" | undefined;
336
336
  singleton?: boolean | undefined;
@@ -354,7 +354,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
354
354
  }[] | undefined;
355
355
  } | undefined;
356
356
  description?: string | undefined;
357
- permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
357
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
358
358
  icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
359
359
  placement?: "right-dock" | undefined;
360
360
  singleton?: boolean | undefined;
@@ -7,6 +7,7 @@ export const PANEL_APP_PERMISSIONS = [
7
7
  "storage",
8
8
  "external.open",
9
9
  "agent.submitPrompt",
10
+ "agent.task",
10
11
  "workspace.info",
11
12
  "workspace.read",
12
13
  "workspace.write",
@@ -14,6 +15,7 @@ export const PANEL_APP_PERMISSIONS = [
14
15
  "audio.transcribe",
15
16
  "credentials.cookies",
16
17
  "automations.manage",
18
+ "process",
17
19
  ];
18
20
  export const PANEL_APP_ICONS = [
19
21
  "panel",
@@ -139,7 +141,7 @@ const PanelAppManifestFields = {
139
141
  icon: z.enum(PANEL_APP_ICONS).default("panel"),
140
142
  placement: z.literal("right-dock").default("right-dock"),
141
143
  singleton: z.boolean().default(true),
142
- permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(12).default([]),
144
+ permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(16).default([]),
143
145
  };
144
146
  /**
145
147
  * Schema v2 keeps one installable Panel App identity while allowing it to
@@ -96,6 +96,14 @@ export interface ComposerOptions {
96
96
  * See MemoryManager.buildInjectionIndex(currentProjectOnly).
97
97
  */
98
98
  memoryCurrentProjectOnly?: boolean;
99
+ /** Isolated runs can omit repository/user instruction discovery entirely. */
100
+ disableInstructions?: boolean;
101
+ /** Isolated runs can omit persistent-memory context entirely. */
102
+ disableMemoryContext?: boolean;
103
+ /** Isolated runs can omit capability-owned volatile context providers. */
104
+ disableCapabilityContext?: boolean;
105
+ /** Isolated runs can omit bound-source metadata. */
106
+ disableSourcesContext?: boolean;
99
107
  }
100
108
  export declare class PromptComposer {
101
109
  private readonly options;
@@ -30,6 +30,8 @@ export class PromptComposer {
30
30
  * Build the userContext prefix message (CLAUDE.md content as <system-reminder>).
31
31
  */
32
32
  buildUserContextMessage() {
33
+ if (this.options.disableInstructions === true)
34
+ return null;
33
35
  const instructions = this.getInstructions();
34
36
  // NOTE: memory is intentionally NOT here. It used to be, but memory mutates
35
37
  // constantly (extraction, recall usage++/lastUsed, approve/demote), and this
@@ -51,6 +53,8 @@ export class PromptComposer {
51
53
  }
52
54
  /** Build volatile context contributed by installed capability modules. */
53
55
  async buildSystemContext() {
56
+ if (this.options.disableCapabilityContext === true)
57
+ return "";
54
58
  const preset = this.options.preset ?? resolveAgentPreset();
55
59
  const parts = await Promise.all((this.options.dynamicContextProviders ?? []).map(async (provider) => {
56
60
  try {
@@ -107,6 +111,8 @@ export class PromptComposer {
107
111
  const [capabilityContext, sourcesContext] = await Promise.all([
108
112
  this.buildSystemContext(),
109
113
  (async () => {
114
+ if (this.options.disableSourcesContext === true)
115
+ return "";
110
116
  try {
111
117
  return (await this.options.sourcesContextProvider?.()) ?? "";
112
118
  }
@@ -265,6 +271,8 @@ export class PromptComposer {
265
271
  return this.cachedInstructions;
266
272
  }
267
273
  getMemoryContext() {
274
+ if (this.options.disableMemoryContext === true)
275
+ return "";
268
276
  try {
269
277
  // Three-layer injection (用户拍板): a compact index merging GLOBAL +
270
278
  // DIGITAL-HUMAN + PROJECT memories. Global memories are surfaced every
@@ -168,6 +168,9 @@ export class ChatSessionManager {
168
168
  getLiveSessionSnapshot() {
169
169
  const sessions = [];
170
170
  this.forEachSession((session) => {
171
+ const sessionManager = this.engineSessionManager(session.engine);
172
+ if (sessionManager?.isEphemeralSession?.(session.id))
173
+ return;
171
174
  sessions.push({
172
175
  sessionId: session.id,
173
176
  busy: session.isBusy(),
@@ -280,6 +283,12 @@ export class ChatSessionManager {
280
283
  sweepIdle() {
281
284
  const cutoff = Date.now() - this.idleTtlMs;
282
285
  for (const [id, s] of [...this.sessions]) {
286
+ // Quick Chat transcripts are process-local by design. Closing one from
287
+ // the generic idle sweeper destroys its inherited context while the
288
+ // renderer still owns (and displays) the panel. Its explicit claim/
289
+ // panel lifecycle is the sole authority that may close it.
290
+ if (id.startsWith("qchat-"))
291
+ continue;
283
292
  if (s.lastActivityAt >= cutoff)
284
293
  continue;
285
294
  if (s.isBusy())
@@ -58,10 +58,39 @@ function runInputError(params) {
58
58
  if (params.injected !== undefined && typeof params.injected !== "boolean") {
59
59
  return "injected must be a boolean";
60
60
  }
61
+ if (params.quickChatClaimId !== undefined &&
62
+ (typeof params.quickChatClaimId !== "string" || params.quickChatClaimId.length === 0)) {
63
+ return "quickChatClaimId must be a non-empty string";
64
+ }
61
65
  if (params.behaviorMode !== undefined &&
62
66
  (typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
63
67
  return `invalid behavior mode: ${String(params.behaviorMode)}`;
64
68
  }
69
+ for (const [field, value, maxItems] of [
70
+ ["toolAllowlist", params.toolAllowlist, 32],
71
+ ["skillAllowlist", params.skillAllowlist, 8],
72
+ ]) {
73
+ if (value === undefined)
74
+ continue;
75
+ if (!Array.isArray(value) ||
76
+ value.length > maxItems ||
77
+ value.some((name) => typeof name !== "string" || name.length === 0 || name.length > 128 || /[\r\n]/.test(name))) {
78
+ return `${field} must be an array of at most ${maxItems} bounded names`;
79
+ }
80
+ }
81
+ if (params.ephemeral !== undefined && typeof params.ephemeral !== "boolean") {
82
+ return "ephemeral must be a boolean";
83
+ }
84
+ if (params.maxTurns !== undefined &&
85
+ (!Number.isInteger(params.maxTurns) || params.maxTurns < 1 || params.maxTurns > 30)) {
86
+ return "maxTurns must be an integer from 1 to 30";
87
+ }
88
+ if (params.maxContextTokens !== undefined &&
89
+ (!Number.isInteger(params.maxContextTokens) ||
90
+ params.maxContextTokens < 4_096 ||
91
+ params.maxContextTokens > 131_072)) {
92
+ return "maxContextTokens must be an integer from 4096 to 131072";
93
+ }
65
94
  if (params.kind !== undefined && (typeof params.kind !== "string" || params.kind.length === 0)) {
66
95
  return `invalid session kind: ${String(params.kind)}`;
67
96
  }
@@ -1080,6 +1109,8 @@ export class AgentServer {
1080
1109
  const sessionConfig = {
1081
1110
  cwd: params.cwd,
1082
1111
  projectTrusted: params.projectTrusted,
1112
+ maxTurns: params.maxTurns,
1113
+ maxContextTokens: params.maxContextTokens,
1083
1114
  goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
1084
1115
  ? params.goal
1085
1116
  : undefined,
@@ -1163,6 +1194,9 @@ export class AgentServer {
1163
1194
  permissionMode: params.permissionMode,
1164
1195
  planMode: params.planMode,
1165
1196
  behaviorMode: params.behaviorMode,
1197
+ toolAllowlist: params.toolAllowlist,
1198
+ skillAllowlist: params.skillAllowlist,
1199
+ ephemeral: params.ephemeral,
1166
1200
  profileParams: params.profileParams,
1167
1201
  workspaceProfile: params.workspaceProfile,
1168
1202
  sessionBrief: params.sessionBrief,
@@ -1290,6 +1324,9 @@ export class AgentServer {
1290
1324
  permissionMode: params.permissionMode,
1291
1325
  planMode: params.planMode,
1292
1326
  behaviorMode: params.behaviorMode,
1327
+ toolAllowlist: params.toolAllowlist,
1328
+ skillAllowlist: params.skillAllowlist,
1329
+ ephemeral: params.ephemeral,
1293
1330
  profileParams: params.profileParams,
1294
1331
  workspaceProfile: params.workspaceProfile,
1295
1332
  sessionBrief: params.sessionBrief,
@@ -80,6 +80,8 @@ export interface RunParams {
80
80
  attachments?: InputAttachmentMeta[];
81
81
  /** Stable id for the user's submit intent; duplicate ids are idempotent. */
82
82
  clientMessageId?: string;
83
+ /** Desktop host ownership generation for process-local Quick Chat runs. */
84
+ quickChatClaimId?: string;
83
85
  /**
84
86
  * Host-generated system reminder. Persisted with an injected marker so disk
85
87
  * transcript readers hide it as a user bubble while retaining the assistant reply.
@@ -116,6 +118,16 @@ export interface RunParams {
116
118
  planMode?: boolean;
117
119
  /** Named behavior profile for a product-specific per-run interaction mode. */
118
120
  behaviorMode?: RunBehaviorMode;
121
+ /** Host-authorized per-run hard tool allowlist. Empty means no tools. */
122
+ toolAllowlist?: string[];
123
+ /** Host-authorized per-run hard Skill allowlist. Empty means no Skills. */
124
+ skillAllowlist?: string[];
125
+ /** Keep a fresh run Session process-local and out of normal Session pickers. */
126
+ ephemeral?: boolean;
127
+ /** Optional fresh-session turn ceiling used by bounded host Tasks. */
128
+ maxTurns?: number;
129
+ /** Optional fresh-session context ceiling used by bounded host Tasks. */
130
+ maxContextTokens?: number;
119
131
  /**
120
132
  * Generic per-run parameters consumed by the active behavior profile
121
133
  * (delivered as non-durable run context; never appended to the task or
@@ -108,7 +108,7 @@ export declare class SessionManager {
108
108
  * Create a session. `qchat-` sessions stay process-local; ordinary sessions
109
109
  * materialize state.json + transcript.jsonl before return.
110
110
  */
111
- create(cwd: string, model: string, provider: string, explicitSessionId?: string, parentSessionId?: string | null, origin?: import("../types.js").SessionOrigin, kind?: SessionKind): SessionBundle;
111
+ create(cwd: string, model: string, provider: string, explicitSessionId?: string, parentSessionId?: string | null, origin?: import("../types.js").SessionOrigin, kind?: SessionKind, ephemeral?: boolean): SessionBundle;
112
112
  /** Whether a persisted or process-local session exists. */
113
113
  exists(sessionId: string): boolean;
114
114
  /**
@@ -236,6 +236,8 @@ export declare class SessionManager {
236
236
  * workspace/profile metadata.
237
237
  */
238
238
  readSessionState(sessionId: string): SessionState | undefined;
239
+ /** Whether a live or persisted Session is explicitly process-local. */
240
+ isEphemeralSession(sessionId: string): boolean;
239
241
  /**
240
242
  * Merge a field-level state update into the latest persisted snapshot.
241
243
  *
@@ -148,26 +148,34 @@ function adoptCompatibilityGoalMutation(state) {
148
148
  // disk. The storage root remains part of the key to preserve identity/data-root
149
149
  // isolation.
150
150
  const processLocalSessionBundles = new Map();
151
- const FORK_COPY_EVENT_TYPES = new Set([
152
- "message",
153
- "tool_use",
154
- "tool_result",
155
- "summary",
156
- "context_transfer",
157
- "range_archive",
158
- "content_replace",
159
- "subagent",
160
- "external_file_changes",
161
- "goal_progress",
162
- "turn_boundary",
163
- "turn_stopped",
164
- "error",
165
- ]);
166
- const FORK_SKIP_EVENT_TYPES = new Set([
167
- "session_meta",
168
- "file_history",
169
- "plan_operation",
170
- ]);
151
+ /**
152
+ * Every known transcript event must make an explicit fork decision. The
153
+ * `satisfies` constraint turns new TranscriptEventType additions into a
154
+ * compile-time error instead of a quick-chat failure discovered at runtime.
155
+ * The runtime fallback below still rejects unknown events from newer or
156
+ * malformed persisted transcripts.
157
+ */
158
+ const FORK_EVENT_POLICY = {
159
+ message: "copy",
160
+ tool_use: "copy",
161
+ tool_result: "copy",
162
+ summary: "copy",
163
+ context_transfer: "copy",
164
+ range_archive: "copy",
165
+ content_replace: "copy",
166
+ file_history: "skip",
167
+ plan_operation: "skip",
168
+ session_meta: "skip",
169
+ subagent: "copy",
170
+ external_file_changes: "copy",
171
+ turn_boundary: "copy",
172
+ // Idempotency receipts belong to the source session and never contribute to
173
+ // model context. Copying them could replay a source response in the child.
174
+ run_result: "skip",
175
+ goal_progress: "copy",
176
+ turn_stopped: "copy",
177
+ error: "copy",
178
+ };
171
179
  const FORK_STAGING_NAME = /^\.pending-fork-[A-Za-z0-9_.-]+-[A-Za-z0-9_-]{8}$/;
172
180
  const FORK_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;
173
181
  const FORK_STAGING_CLEANUP_LIMIT = 32;
@@ -366,7 +374,7 @@ export class SessionManager {
366
374
  * Create a session. `qchat-` sessions stay process-local; ordinary sessions
367
375
  * materialize state.json + transcript.jsonl before return.
368
376
  */
369
- create(cwd, model, provider, explicitSessionId, parentSessionId, origin, kind = "work") {
377
+ create(cwd, model, provider, explicitSessionId, parentSessionId, origin, kind = "work", ephemeral = false) {
370
378
  // External callers may pass any string; nanoid output is trusted. Either
371
379
  // way the ID gets joined into a filesystem path, so the public entry
372
380
  // point validates before that join.
@@ -394,7 +402,7 @@ export class SessionManager {
394
402
  // new top-level session (key present, null) apart from a legacy session
395
403
  // (key absent) and from a sub-agent (key present, non-empty string).
396
404
  parentSessionId: parentSessionId ?? null,
397
- ...(sessionId.startsWith("qchat-") ? { ephemeral: true } : {}),
405
+ ...(ephemeral || sessionId.startsWith("qchat-") ? { ephemeral: true } : {}),
398
406
  ...(origin ? { origin } : {}),
399
407
  };
400
408
  if (isEphemeralSessionState(state)) {
@@ -1049,6 +1057,10 @@ export class SessionManager {
1049
1057
  return undefined;
1050
1058
  }
1051
1059
  }
1060
+ /** Whether a live or persisted Session is explicitly process-local. */
1061
+ isEphemeralSession(sessionId) {
1062
+ return this.readSessionState(sessionId)?.ephemeral === true;
1063
+ }
1052
1064
  /**
1053
1065
  * Merge a field-level state update into the latest persisted snapshot.
1054
1066
  *
@@ -1569,7 +1581,10 @@ export class SessionManager {
1569
1581
  return this.freezeForkSnapshot(sourceSessionId, sourceState, parsed.events, throughEventId, snapshotMode);
1570
1582
  }
1571
1583
  freezeForkSnapshot(sourceSessionId, sourceState, events, throughEventId, snapshotMode) {
1572
- const sourceEvents = structuredClone([...events]);
1584
+ // Both process-local and disk readers provide a snapshot array. Fork
1585
+ // selection is synchronous, so keep event references here and clone once
1586
+ // when the independently-owned target transcript is constructed below.
1587
+ const sourceEvents = [...events];
1573
1588
  let frozen = sourceEvents;
1574
1589
  const effectiveCursor = snapshotMode === "completed" ? sourceState.completedThroughEventId : throughEventId;
1575
1590
  if (snapshotMode === "completed" && effectiveCursor === undefined) {
@@ -1600,12 +1615,13 @@ export class SessionManager {
1600
1615
  }
1601
1616
  const copiedEvents = [];
1602
1617
  for (const event of frozen) {
1603
- if (FORK_SKIP_EVENT_TYPES.has(event.type))
1618
+ const policy = FORK_EVENT_POLICY[event.type];
1619
+ if (policy === "skip")
1604
1620
  continue;
1605
- if (!FORK_COPY_EVENT_TYPES.has(event.type)) {
1621
+ if (policy !== "copy") {
1606
1622
  throw new SessionError(`Unsupported transcript event in fork: ${String(event.type)}`);
1607
1623
  }
1608
- copiedEvents.push(structuredClone(event));
1624
+ copiedEvents.push(event);
1609
1625
  }
1610
1626
  validateForkToolPairs(copiedEvents);
1611
1627
  return { sourceState: structuredClone(sourceState), copiedEvents };
@@ -310,7 +310,10 @@ const BUILTIN_CONTRIBUTIONS = [
310
310
  timeoutMs: 1_800_000, // 30min — sub-agent runs may execute many tool calls
311
311
  },
312
312
  execute: agentTool,
313
- exposure: expose(HARNESS_TAGS, { defaultPermissionRules: allow(agentToolDef.name) }),
313
+ exposure: expose(HARNESS_TAGS, {
314
+ defaultPermissionRules: allow(agentToolDef.name),
315
+ availability: (ctx) => ctx.behaviorProfile !== "quickChatRestricted",
316
+ }),
314
317
  },
315
318
  {
316
319
  definition: {
@@ -449,8 +449,6 @@ function isRegisteredSkillResourceRead(resolved) {
449
449
  return false;
450
450
  if (isSkillTreeResource(resolved, join(codeShellRoot, "skills")))
451
451
  return true;
452
- if (isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot))
453
- return true;
454
452
  let cacheRoot;
455
453
  try {
456
454
  cacheRoot = realpathSync(join(codeShellRoot, "plugins", "cache"));
@@ -479,20 +477,26 @@ function isRegisteredSkillResourceRead(resolved) {
479
477
  return false;
480
478
  }
481
479
  /**
482
- * Panel App Skills live under the otherwise-sensitive
483
- * `~/.code-shell/panel-apps` tree. Installation already reviews and copies the
484
- * package, and the Skill scanner exposes only entries declared by the app
485
- * manifest. Mirror that exact boundary here so reading a Skill reference does
486
- * not trigger a second approval prompt.
480
+ * Installed Panel Apps live under the otherwise-sensitive
481
+ * `~/.code-shell/panel-apps` tree. Their package contents are reviewed and
482
+ * copied by the installer, so ordinary source, manifests, assets and declared
483
+ * Agent resources should not require a second approval merely because their
484
+ * parent directory is `~/.code-shell`.
487
485
  *
488
- * Registry, app root, manifest, declared SKILL.md and target are all
489
- * realpathed and containment-checked. This deliberately does not trust an
490
- * undeclared Skill directory or a symlink escaping the installed app.
486
+ * Keep the exception read-only at the call site and require a valid installed
487
+ * registry entry plus a matching V2 manifest. Registry, app root, manifest and
488
+ * target are all realpathed and containment-checked. Credential-shaped files
489
+ * are still caught by `SENSITIVE_FILE_PATTERNS` before this exception, and a
490
+ * symlink escaping the installed package never inherits its read authority.
491
491
  */
492
- function isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot) {
492
+ function isInstalledPanelAppResourceRead(resolved) {
493
+ let codeShellRoot;
493
494
  let appsRoot;
494
495
  let registryPath;
495
496
  try {
497
+ codeShellRoot = realpathSync(join(configuredUserHome(), ".code-shell"));
498
+ if (!isInsideDir(resolved, codeShellRoot))
499
+ return false;
496
500
  appsRoot = realpathSync(join(codeShellRoot, "panel-apps"));
497
501
  if (!isInsideDir(appsRoot, codeShellRoot))
498
502
  return false;
@@ -526,29 +530,10 @@ function isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot) {
526
530
  if (!isInsideDir(manifestPath, appRoot) || !statSync(manifestPath).isFile())
527
531
  continue;
528
532
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
529
- if (manifest.schemaVersion !== 2 ||
530
- manifest.id !== id ||
531
- !Array.isArray(manifest.agent?.skills)) {
533
+ if (manifest.schemaVersion !== 2 || manifest.id !== id)
532
534
  continue;
533
- }
534
- for (const skillEntry of manifest.agent.skills) {
535
- if (typeof skillEntry !== "string")
536
- continue;
537
- const segments = skillEntry.split("/");
538
- if (segments.length !== 4 ||
539
- segments[0] !== "agent" ||
540
- segments[1] !== "skills" ||
541
- !/^[a-z][a-z0-9-]{0,63}$/.test(segments[2] ?? "") ||
542
- segments[3] !== "SKILL.md") {
543
- continue;
544
- }
545
- const skillManifest = realpathSync(join(appRoot, ...segments));
546
- if (!isInsideDir(skillManifest, appRoot) || !statSync(skillManifest).isFile())
547
- continue;
548
- const skillRoot = dirname(skillManifest);
549
- if (isInsideDir(resolved, skillRoot))
550
- return true;
551
- }
535
+ if (isInsideDir(resolved, appRoot))
536
+ return true;
552
537
  }
553
538
  catch {
554
539
  // A stale, malformed, or tampered Panel App entry grants no read access.
@@ -652,6 +637,13 @@ export function classifyPath(rawPath, opts) {
652
637
  resolvedPath: resolved,
653
638
  };
654
639
  }
640
+ if (opts.operation === "read" && !sensitiveFile && isInstalledPanelAppResourceRead(resolved)) {
641
+ return {
642
+ decision: "allow",
643
+ reason: "installed Panel App resource read",
644
+ resolvedPath: resolved,
645
+ };
646
+ }
655
647
  // Sensitive: write is always denied, read always asks. Workspace placement
656
648
  // doesn't soften the rule — an `.env` in the project still asks on read.
657
649
  if (sensitiveLabel) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Core engine for code-shell — agent orchestration, tool execution, hooks, protocol. UI-agnostic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",