@cjhyy/code-shell-core 0.8.5 → 0.8.7

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.
@@ -62,11 +62,15 @@ export interface CronJob {
62
62
  * job_error carries the message `fire()` would otherwise swallow.
63
63
  */
64
64
  export interface CronJobLifecycleEvent {
65
- type: "job_start" | "job_end" | "job_stopped" | "job_cancelled" | "job_error";
65
+ type: "job_start" | "job_end" | "job_stopped" | "job_cancelled" | "job_error" | "job_missed";
66
66
  job: CronJob;
67
67
  durationMs?: number;
68
68
  reason?: string;
69
69
  error?: string;
70
+ /** Planned wall-clock instant that was skipped after the host woke too late. */
71
+ scheduledFor?: number;
72
+ /** Time the scheduler observed the skipped occurrence. */
73
+ observedAt?: number;
70
74
  }
71
75
  /** Optional semantic outcome returned by an executor after a non-throwing run. */
72
76
  export interface CronExecutionOutcome {
@@ -128,10 +132,10 @@ export declare class CronScheduler {
128
132
  setStore(store: CronStore): void;
129
133
  setExecutor(fn: (job: CronJob, signal: AbortSignal) => Promise<void | CronExecutionOutcome>): void;
130
134
  /**
131
- * Observe job execution lifecycle
132
- * (job_start job_end | job_stopped | job_cancelled | job_error). Hosts
133
- * wire this to their notification surface so scheduled runs — especially
134
- * failures, which `fire()` otherwise swallows — are no longer silent.
135
+ * Observe job execution lifecycle. A real execution emits job_start followed
136
+ * by one terminal event; a sleep/wake skip emits job_missed without pretending
137
+ * the job started. Hosts wire this to their notification surface so failures
138
+ * and intentionally skipped occurrences are no longer silent.
135
139
  * Listener errors are swallowed; observation never affects scheduling.
136
140
  */
137
141
  setJobEventListener(listener: ((event: CronJobLifecycleEvent) => void) | undefined): void;
@@ -88,10 +88,10 @@ export class CronScheduler {
88
88
  this.onExecute = fn;
89
89
  }
90
90
  /**
91
- * Observe job execution lifecycle
92
- * (job_start job_end | job_stopped | job_cancelled | job_error). Hosts
93
- * wire this to their notification surface so scheduled runs — especially
94
- * failures, which `fire()` otherwise swallows — are no longer silent.
91
+ * Observe job execution lifecycle. A real execution emits job_start followed
92
+ * by one terminal event; a sleep/wake skip emits job_missed without pretending
93
+ * the job started. Hosts wire this to their notification surface so failures
94
+ * and intentionally skipped occurrences are no longer silent.
95
95
  * Listener errors are swallowed; observation never affects scheduling.
96
96
  */
97
97
  setJobEventListener(listener) {
@@ -167,6 +167,14 @@ export class CronScheduler {
167
167
  if (Number.isFinite(n) && n > maxId)
168
168
  maxId = n;
169
169
  if (!prev) {
170
+ const observedAt = Date.now();
171
+ const missedWhileStopped = arm &&
172
+ this.executionEnabled &&
173
+ job.enabled &&
174
+ typeof job.nextRun === "number" &&
175
+ isCronMisfire(job.nextRun, observedAt)
176
+ ? job.nextRun
177
+ : undefined;
170
178
  this.jobs.set(job.id, job);
171
179
  if (arm && job.enabled) {
172
180
  this.arm(job);
@@ -174,6 +182,15 @@ export class CronScheduler {
174
182
  else {
175
183
  this.refreshNextRunForDisplay(job);
176
184
  }
185
+ if (missedWhileStopped !== undefined) {
186
+ this.persistRunStats(job);
187
+ this.emitJobEvent({
188
+ type: "job_missed",
189
+ job,
190
+ scheduledFor: missedWhileStopped,
191
+ observedAt,
192
+ });
193
+ }
177
194
  continue;
178
195
  }
179
196
  const definitionChanged = this.jobDefinitionChanged(prev, job);
@@ -600,8 +617,11 @@ export class CronScheduler {
600
617
  // Interval jobs get the same misfire semantics cron already had: if we
601
618
  // wake far PAST the target (host slept through it), skip this occurrence
602
619
  // and re-arm on the next absolute slot instead of firing late.
603
- if (isCronMisfire(scheduledFor, Date.now())) {
620
+ const observedAt = Date.now();
621
+ if (isCronMisfire(scheduledFor, observedAt)) {
604
622
  rearm();
623
+ this.persistRunStats(job);
624
+ this.emitJobEvent({ type: "job_missed", job, scheduledFor, observedAt });
605
625
  return;
606
626
  }
607
627
  void this.fire(job, rearm).then((ran) => {
@@ -636,9 +656,12 @@ export class CronScheduler {
636
656
  // resume). If we wake too far PAST the scheduled instant, this is a
637
657
  // misfire: skip running and re-arm to the next correct occurrence rather
638
658
  // than running at the wrong time.
639
- if (isCronMisfire(scheduledFor, Date.now())) {
659
+ const observedAt = Date.now();
660
+ if (isCronMisfire(scheduledFor, observedAt)) {
640
661
  if (job.enabled)
641
662
  this.armCron(job);
663
+ this.persistRunStats(job);
664
+ this.emitJobEvent({ type: "job_missed", job, scheduledFor, observedAt });
642
665
  return;
643
666
  }
644
667
  const rearm = () => {
@@ -1956,7 +1956,11 @@ export class Engine {
1956
1956
  disabledPlugins,
1957
1957
  skillAllowlist: this.config.skillAllowlist,
1958
1958
  memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
1959
- goalToolState: { hasGoal: hasRunnableGoal },
1959
+ goalToolState: !profile?.allowedToolNames ||
1960
+ profile.allowedToolNames.has("complete_goal") ||
1961
+ profile.allowedToolNames.has("cancel_goal")
1962
+ ? { hasGoal: hasRunnableGoal }
1963
+ : undefined,
1960
1964
  capabilityPromptSections: this.capabilityPromptSections,
1961
1965
  dynamicContextProviders: this.capabilityDynamicContextProviders,
1962
1966
  getSettingsManager: () => this.getSettingsManager(),
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.5";
6
+ export declare const VERSION = "0.8.7";
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.5";
6
+ export const VERSION = "0.8.7";
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"];
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", "credentials.cookies", "automations.manage"];
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"]>, "many">>;
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", "credentials.cookies", "automations.manage"]>, "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")[];
124
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
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")[] | undefined;
147
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | 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"]>, "many">>;
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", "credentials.cookies", "automations.manage"]>, "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")[];
238
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
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")[] | undefined;
279
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | 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")[];
286
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
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")[];
301
+ permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
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")[] | undefined;
333
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | 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")[] | undefined;
357
+ permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | 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;
@@ -11,6 +11,8 @@ export const PANEL_APP_PERMISSIONS = [
11
11
  "workspace.read",
12
12
  "workspace.write",
13
13
  "notifications.send",
14
+ "credentials.cookies",
15
+ "automations.manage",
14
16
  ];
15
17
  export const PANEL_APP_ICONS = [
16
18
  "panel",
@@ -136,7 +138,7 @@ const PanelAppManifestFields = {
136
138
  icon: z.enum(PANEL_APP_ICONS).default("panel"),
137
139
  placement: z.literal("right-dock").default("right-dock"),
138
140
  singleton: z.boolean().default(true),
139
- permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(8).default([]),
141
+ permissions: z.array(z.enum(PANEL_APP_PERMISSIONS)).max(12).default([]),
140
142
  };
141
143
  /**
142
144
  * Schema v2 keeps one installable Panel App identity while allowing it to
@@ -186,4 +188,13 @@ export const PanelAppManifest = z
186
188
  message: "workspace.read and workspace.write require context.workspace",
187
189
  });
188
190
  }
191
+ if (value.permissions.includes("automations.manage") &&
192
+ (!value.permissions.includes("context.session") ||
193
+ !value.permissions.includes("context.workspace"))) {
194
+ ctx.addIssue({
195
+ code: z.ZodIssueCode.custom,
196
+ path: ["permissions"],
197
+ message: "automations.manage requires context.session and context.workspace",
198
+ });
199
+ }
189
200
  });
@@ -134,8 +134,8 @@ export class PromptComposer {
134
134
  if (!this.options.goalToolState)
135
135
  return "";
136
136
  return this.options.goalToolState.hasGoal
137
- ? "当前存在 active goal。只有在目标完全完成时才可调用 complete_goal;只有用户明确要求取消/停止/放弃该目标时才可调用 cancel_goal"
138
- : "当前没有 active goal。不要调用 complete_goal/cancel_goal;如果误调用,系统会拒绝。";
137
+ ? "Goal 工具状态:本会话存在一个持久 Goal。只有在该 Goal 完全完成时才可调用 complete_goal;只有用户明确要求取消/停止/放弃该 Goal 时才可调用 cancel_goal。此状态不代表后台任务或其他 Session 的运行状态,不要把它改写成系统是否空闲。"
138
+ : "Goal 工具状态:本会话没有持久 Goal,因此不要调用 complete_goal/cancel_goal;如果误调用,系统会拒绝。这只描述 Goal 工具的可用状态,不代表没有后台任务、没有运行中的 Session 或系统正在待命,也不要向用户复述为任务状态。";
139
139
  }
140
140
  invalidateCache(sectionName) {
141
141
  this.sectionCache.invalidate(sectionName);
@@ -1,11 +1,11 @@
1
1
  ## Browser automation (browser_observe / browser_act / browser_navigate)
2
2
 
3
3
  - When a task needs you to operate a real web page — search a site, open a result, read/extract an article, fill a form — use the three browser tools. They drive a task-owned background tab in CodeShell's in-app browser profile. The tab can share that profile's login state, but it never controls a user-opened tab unless the user explicitly grants that exact tab. The user's regular Chrome profile is available only through an explicit Chrome extension grant.
4
- - The Browser Runtime starts lazily in the background. Call `browser_navigate{url}`, then `browser_act{action:"wait"}` and `browser_observe`. If login, 2FA, or a high-consequence action needs the user, CodeShell reveals the exact same runtime-owned in-app target for takeover. Scheduled and explicitly isolated work may use a separate Dedicated Playwright profile instead.
4
+ - The Browser Runtime starts lazily in the background. Call `browser_navigate{url}`, then `browser_act{action:"wait"}` and `browser_observe`. If login, 2FA, CAPTCHA, or a high-consequence action needs the user—or the user explicitly asks to see the page—call `browser_act{action:"request_takeover"}`. CodeShell reveals the exact same runtime-owned in-app target; do not send a second link and claim it is the page you operated. Scheduled and explicitly isolated work may use a separate Dedicated Playwright profile instead.
5
5
  - The loop is observe → act → re-observe: ALWAYS `browser_observe` first (default mode `snapshot`) to see the page's interactive elements (each tagged `[ref=eN]`), then `browser_act` on elements BY THAT ref: `{action:"click",ref}`, `{action:"type",ref,text}`, `{action:"select",ref,value}` (native `<select>`), `{action:"hover",ref}`. Refs are only valid for the most recent snapshot — after any navigation, content-loading click, or `{action:"scroll"}`, run `{action:"wait"}` then `browser_observe` again. If an action says a ref is stale, re-observe.
6
6
  - To submit a search: `browser_act{action:"type",ref,text}` into the search box, then `browser_act{action:"press_key",key:"Enter"}`, then `{action:"wait"}` + `browser_observe`. `press_key` also does Tab / Escape / arrows / combos like `Control+a`.
7
7
  - To extract/summarize page content, navigate/click to it, `browser_act{action:"wait"}`, then `browser_observe{mode:"read"}`. If it returns `nextCursor`, continue with `browser_observe{mode:"read",cursor:"..."}` until `Read: complete`. Do NOT scroll merely to obtain the next text chunk. Use scroll only to load lazy/infinite content; stop when it reports `NO_PROGRESS` or `(end)`. For real link/image/video URLs, use `browser_observe{mode:"extract"}` (media is tagged [ref=imgN/vidN]).
8
8
  - To SEE an actual image's content (e.g. a 小红书 笔记配图 or product photo), `browser_observe{mode:"extract"}` to get image refs, then `browser_observe{mode:"image", refs:["img3"]}` — it loads the real pixels (works behind hotlink protection). A vidN ref grabs the video's current frame. To see the rendered layout/canvas/chart, `browser_observe{mode:"vision"}` (optionally `ref` for one region). Use images sparingly — they cost tokens; prefer snapshot/read. (Both need a vision-capable model; on a non-vision model they're skipped.) Do NOT repeat `vision` on the same page hoping for a clearer view — one screenshot is all you get. If snapshot/screenshot don't reveal what you need (infinite-scroll/canvas feeds), scroll + re-observe or extract the URLs and act on them — don't loop screenshots.
9
9
  - Multiple tabs: `browser_act{action:"list_tabs"}` shows open tabs (tabId/url/title/active); `browser_act{action:"switch_tab", tabId}` makes another the active one (then re-observe). Any action also accepts `tabId` to target a specific tab (it switches first). Refs are per-tab — re-observe after switching.
10
- - If `browser_observe` reports a sign-in is required (or you hit a login wall / 2FA), STOP and ask the user to log in manually in the revealed Browser Runtime window, then continue do not attempt to enter credentials yourself. Sensitive actions (payment, delete, entering card/password values) require user approval.
10
+ - If `browser_observe` reports a sign-in is required (or you hit a login wall / 2FA / CAPTCHA), call `browser_act{action:"request_takeover"}`, STOP, and ask the user to act in the revealed Browser Runtime window. Continue only after the user finishes; do not attempt to enter credentials yourself. Sensitive actions (payment, delete, entering card/password values) require user approval.
11
11
  - If the tools report that no Browser Runtime is available, the current host does not provide browser automation — say so rather than retrying.
@@ -2790,6 +2790,7 @@ export class AgentServer {
2790
2790
  makeBrowserBridge(session, sessionId) {
2791
2791
  const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
2792
2792
  return {
2793
+ requestHumanTakeover: () => call("requestTakeover", {}),
2793
2794
  snapshot: () => call("snapshot", {}),
2794
2795
  click: (ref) => call("click", { ref }),
2795
2796
  type: (ref, text) => call("type", { ref, text }),
@@ -145,6 +145,9 @@ export interface BrowserImageData {
145
145
  detail?: string;
146
146
  }
147
147
  export interface BrowserBridge {
148
+ /** Reveal the exact task-owned browser target so the user can complete a
149
+ * login, 2FA, CAPTCHA, or another interaction that requires human control. */
150
+ requestHumanTakeover?(): Promise<BrowserResult>;
148
151
  snapshot(): Promise<BrowserSnapshot>;
149
152
  click(ref: string): Promise<BrowserResult>;
150
153
  type(ref: string, text: string): Promise<BrowserResult>;
@@ -209,6 +209,9 @@ export const browserActToolDef = {
209
209
  "- hover {ref}: hover to reveal menus/tooltips.\n" +
210
210
  "- scroll {direction: up|down, amount?}: scroll the page, then re-observe.\n" +
211
211
  "- wait {timeout_ms?}: wait for the page to finish loading before observing.\n" +
212
+ "- request_takeover: reveal the exact task-owned Browser Runtime page so the " +
213
+ "user can see it and complete login, 2FA, CAPTCHA, or another required manual step. " +
214
+ "Use only when the user asks to see the page or human interaction is required.\n" +
212
215
  "- list_tabs: list open browser tabs (tabId, url, title, which is active).\n" +
213
216
  "- switch_tab {tabId}: make another tab the active one that actions drive.\n" +
214
217
  "Pass tabId on any action to target a specific tab (switches to it first). " +
@@ -226,6 +229,7 @@ export const browserActToolDef = {
226
229
  "hover",
227
230
  "scroll",
228
231
  "wait",
232
+ "request_takeover",
229
233
  "list_tabs",
230
234
  "switch_tab",
231
235
  ],
@@ -260,6 +264,15 @@ export async function browserActTool(args, ctx) {
260
264
  return `Error: could not switch to tab ${tabId} — ${sw.detail ?? "not found"}`;
261
265
  }
262
266
  switch (action) {
267
+ case "request_takeover": {
268
+ if (!b.requestHumanTakeover) {
269
+ return "Error: this Browser Runtime cannot reveal its page for user takeover";
270
+ }
271
+ const r = await b.requestHumanTakeover();
272
+ return r.ok
273
+ ? `Browser Runtime is visible for user takeover${r.detail ? ` — ${r.detail}` : ""}`
274
+ : `Error: ${r.detail ?? "could not reveal Browser Runtime"}`;
275
+ }
263
276
  case "list_tabs": {
264
277
  const tabs = await b.listTabs();
265
278
  if (tabs.length === 0)
@@ -432,9 +432,10 @@ function isSkillTreeResource(resolved, skillsRoot) {
432
432
  *
433
433
  * Keep the exception narrow:
434
434
  * - read-only (the caller checks the operation);
435
- * - user Skills, or an installed plugin recorded in the V2 registry;
435
+ * - user Skills, an installed plugin recorded in the V2 registry, or a
436
+ * declared Skill in the installed Panel App registry;
436
437
  * - plugin installs must realpath beneath the managed plugin cache;
437
- * - the target must remain inside a directory containing a real SKILL.md.
438
+ * - the target must remain inside the registered Skill directory.
438
439
  */
439
440
  function isRegisteredSkillResourceRead(resolved) {
440
441
  let codeShellRoot;
@@ -448,6 +449,8 @@ function isRegisteredSkillResourceRead(resolved) {
448
449
  return false;
449
450
  if (isSkillTreeResource(resolved, join(codeShellRoot, "skills")))
450
451
  return true;
452
+ if (isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot))
453
+ return true;
451
454
  let cacheRoot;
452
455
  try {
453
456
  cacheRoot = realpathSync(join(codeShellRoot, "plugins", "cache"));
@@ -475,6 +478,84 @@ function isRegisteredSkillResourceRead(resolved) {
475
478
  }
476
479
  return false;
477
480
  }
481
+ /**
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.
487
+ *
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.
491
+ */
492
+ function isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot) {
493
+ let appsRoot;
494
+ let registryPath;
495
+ try {
496
+ appsRoot = realpathSync(join(codeShellRoot, "panel-apps"));
497
+ if (!isInsideDir(appsRoot, codeShellRoot))
498
+ return false;
499
+ registryPath = realpathSync(join(appsRoot, "installed.json"));
500
+ if (!isInsideDir(registryPath, appsRoot))
501
+ return false;
502
+ }
503
+ catch {
504
+ return false;
505
+ }
506
+ let registry;
507
+ try {
508
+ registry = JSON.parse(readFileSync(registryPath, "utf-8"));
509
+ }
510
+ catch {
511
+ return false;
512
+ }
513
+ if (registry.version !== 1 || !Array.isArray(registry.apps))
514
+ return false;
515
+ for (const rawEntry of registry.apps) {
516
+ if (!rawEntry || typeof rawEntry !== "object")
517
+ continue;
518
+ const id = rawEntry.id;
519
+ if (typeof id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(id))
520
+ continue;
521
+ try {
522
+ const appRoot = realpathSync(join(appsRoot, id));
523
+ if (appRoot === appsRoot || !isInsideDir(appRoot, appsRoot))
524
+ continue;
525
+ const manifestPath = realpathSync(join(appRoot, ".codeshell-panel", "panel.json"));
526
+ if (!isInsideDir(manifestPath, appRoot) || !statSync(manifestPath).isFile())
527
+ continue;
528
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
529
+ if (manifest.schemaVersion !== 2 ||
530
+ manifest.id !== id ||
531
+ !Array.isArray(manifest.agent?.skills)) {
532
+ 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
+ }
552
+ }
553
+ catch {
554
+ // A stale, malformed, or tampered Panel App entry grants no read access.
555
+ }
556
+ }
557
+ return false;
558
+ }
478
559
  /**
479
560
  * Returns the matching sensitive-dir entry (with the user's home prefix) if
480
561
  * `resolved` lives underneath any sensitive directory, else undefined.
@@ -564,9 +645,7 @@ export function classifyPath(rawPath, opts) {
564
645
  // files through the ordinary Read tool as well. A credential-shaped basename
565
646
  // (.env, token.txt, key files, ...) deliberately keeps the sensitive-file
566
647
  // gate even inside a Skill tree.
567
- if (opts.operation === "read" &&
568
- !sensitiveFile &&
569
- isRegisteredSkillResourceRead(resolved)) {
648
+ if (opts.operation === "read" && !sensitiveFile && isRegisteredSkillResourceRead(resolved)) {
570
649
  return {
571
650
  decision: "allow",
572
651
  reason: "registered Skill resource read",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
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",