@cjhyy/code-shell-core 0.9.5 → 0.9.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.
Files changed (89) hide show
  1. package/dist/automation/scheduler.d.ts +3 -0
  2. package/dist/automation/scheduler.js +21 -0
  3. package/dist/context/manager.d.ts +8 -2
  4. package/dist/context/manager.js +20 -4
  5. package/dist/context/notes.d.ts +39 -0
  6. package/dist/context/notes.js +314 -0
  7. package/dist/credentials/access.d.ts +9 -1
  8. package/dist/credentials/access.js +15 -4
  9. package/dist/credentials/store.d.ts +11 -0
  10. package/dist/credentials/store.js +44 -9
  11. package/dist/credentials/types.d.ts +4 -0
  12. package/dist/credentials/types.js +4 -0
  13. package/dist/credentials/use-credential-tool.js +12 -2
  14. package/dist/engine/engine-workspace-authority.js +10 -3
  15. package/dist/engine/engine.d.ts +8 -4
  16. package/dist/engine/engine.js +115 -15
  17. package/dist/engine/prompt-cache-diagnostics.js +10 -1
  18. package/dist/engine/run-goal.js +7 -3
  19. package/dist/engine/run-tooling.d.ts +3 -0
  20. package/dist/engine/run-tooling.js +25 -23
  21. package/dist/engine/run-types.d.ts +20 -0
  22. package/dist/engine/run-workspace.js +5 -21
  23. package/dist/engine/subagent-spawner.d.ts +3 -0
  24. package/dist/engine/subagent-spawner.js +48 -17
  25. package/dist/engine/turn-loop.d.ts +14 -4
  26. package/dist/engine/turn-loop.js +120 -28
  27. package/dist/engine/types.d.ts +19 -0
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +2 -2
  30. package/dist/links/index.d.ts +1 -0
  31. package/dist/links/index.js +1 -0
  32. package/dist/links/link-action-tool.d.ts +2 -1
  33. package/dist/links/link-action-tool.js +80 -44
  34. package/dist/links/status.d.ts +53 -0
  35. package/dist/links/status.js +175 -0
  36. package/dist/llm/prompt-cache.d.ts +35 -3
  37. package/dist/llm/prompt-cache.js +63 -3
  38. package/dist/llm/providers/openai.d.ts +3 -0
  39. package/dist/llm/providers/openai.js +57 -14
  40. package/dist/prompt/section-loader.js +1 -0
  41. package/dist/prompt/sections/browser.md +4 -2
  42. package/dist/prompt/sections/context-notes.md +9 -0
  43. package/dist/protocol/background-result-wakeup.d.ts +8 -1
  44. package/dist/protocol/background-result-wakeup.js +76 -38
  45. package/dist/protocol/chat-session-manager.d.ts +7 -1
  46. package/dist/protocol/chat-session-manager.js +44 -8
  47. package/dist/protocol/chat-session.d.ts +2 -0
  48. package/dist/protocol/chat-session.js +4 -1
  49. package/dist/protocol/server.d.ts +4 -0
  50. package/dist/protocol/server.js +355 -62
  51. package/dist/protocol/session-message-result.d.ts +12 -0
  52. package/dist/protocol/session-message-result.js +42 -0
  53. package/dist/protocol/session-message-workspace.d.ts +21 -0
  54. package/dist/protocol/session-message-workspace.js +57 -0
  55. package/dist/protocol/types.d.ts +4 -0
  56. package/dist/session/session-manager.js +8 -7
  57. package/dist/session/session-message.d.ts +17 -2
  58. package/dist/session/transcript.d.ts +17 -0
  59. package/dist/session/transcript.js +271 -16
  60. package/dist/settings/schema.d.ts +9 -0
  61. package/dist/settings/schema.js +4 -0
  62. package/dist/themes/paths.js +20 -1
  63. package/dist/tool-system/browser-bridge.d.ts +21 -1
  64. package/dist/tool-system/browser-discovery.d.ts +6 -0
  65. package/dist/tool-system/browser-discovery.js +17 -0
  66. package/dist/tool-system/builtin/agent.js +23 -9
  67. package/dist/tool-system/builtin/browser-tools.js +30 -9
  68. package/dist/tool-system/builtin/context-notes.d.ts +12 -0
  69. package/dist/tool-system/builtin/context-notes.js +188 -0
  70. package/dist/tool-system/builtin/index.js +50 -3
  71. package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
  72. package/dist/tool-system/builtin/mcp-tools.js +10 -10
  73. package/dist/tool-system/builtin/send-message-to-session.js +19 -3
  74. package/dist/tool-system/builtin/tool-search.js +15 -3
  75. package/dist/tool-system/context.d.ts +9 -0
  76. package/dist/tool-system/executor.js +5 -3
  77. package/dist/tool-system/mcp-compat.d.ts +3 -0
  78. package/dist/tool-system/mcp-compat.js +51 -0
  79. package/dist/tool-system/mcp-manager.d.ts +27 -26
  80. package/dist/tool-system/mcp-manager.js +273 -111
  81. package/dist/tool-system/mcp-workspace.d.ts +18 -0
  82. package/dist/tool-system/mcp-workspace.js +56 -0
  83. package/dist/tool-system/permission.d.ts +6 -0
  84. package/dist/tool-system/permission.js +45 -9
  85. package/dist/tool-system/plan-mode-allowlist.js +5 -0
  86. package/dist/tool-system/sandbox/seatbelt.js +71 -2
  87. package/dist/tool-system/session-tool-host.js +9 -1
  88. package/dist/types.d.ts +4 -2
  89. package/package.json +1 -1
@@ -100,7 +100,10 @@ export declare class InteractiveApprovalBackend implements ApprovalBackend {
100
100
  private promptFn;
101
101
  private legacyPromptTurn;
102
102
  private legacyContext;
103
+ private requestGuard?;
103
104
  constructor(approvalRouter?: ApprovalRouter);
105
+ /** Host lifecycle checks apply even to cached grants and queued prompts. */
106
+ setRequestGuard(guard: (request: ApprovalRequest) => ApprovalResult | undefined): void;
104
107
  setPromptFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
105
108
  clearPromptFn(): void;
106
109
  /**
@@ -220,6 +223,9 @@ export declare class PermissionClassifier {
220
223
  handleAsk(toolName: string, args: Record<string, unknown>, reason?: string, opts?: {
221
224
  sessionId?: string;
222
225
  }): Promise<boolean>;
226
+ handleAskResult(toolName: string, args: Record<string, unknown>, reason?: string, opts?: {
227
+ sessionId?: string;
228
+ }): Promise<ApprovalResult>;
223
229
  /** Get denial warning message if the model keeps getting denied. */
224
230
  getDenialWarning(toolName: string): string | undefined;
225
231
  private matchesRule;
@@ -106,12 +106,16 @@ export class HeadlessApprovalBackend {
106
106
  case "approve-all":
107
107
  return { approved: true };
108
108
  case "deny-all":
109
- return { approved: false, reason: "headless deny-all mode" };
109
+ return { approved: false, failure: "policy_denied", reason: "headless deny-all mode" };
110
110
  case "approve-read-only": {
111
111
  if (READ_ONLY_TOOLS.has(req.toolName)) {
112
112
  return { approved: true };
113
113
  }
114
- return { approved: false, reason: "read-only mode: write operations denied" };
114
+ return {
115
+ approved: false,
116
+ failure: "policy_denied",
117
+ reason: "read-only mode: write operations denied",
118
+ };
115
119
  }
116
120
  }
117
121
  }
@@ -140,6 +144,7 @@ export class AutoApprovalBackend {
140
144
  }
141
145
  return {
142
146
  approved: false,
147
+ failure: "unavailable",
143
148
  reason: "auto mode: high-risk operation denied (no interactive approval available)",
144
149
  };
145
150
  }
@@ -157,6 +162,7 @@ export class AutoApprovalBackend {
157
162
  }
158
163
  return {
159
164
  approved: false,
165
+ failure: "unavailable",
160
166
  reason: "auto mode: medium-risk operation denied (no interactive approval available)",
161
167
  };
162
168
  }
@@ -223,9 +229,14 @@ export class InteractiveApprovalBackend {
223
229
  savedProjectRules: [],
224
230
  onProjectRules: null,
225
231
  };
232
+ requestGuard;
226
233
  constructor(approvalRouter = new ApprovalRouter()) {
227
234
  this.approvalRouter = approvalRouter;
228
235
  }
236
+ /** Host lifecycle checks apply even to cached grants and queued prompts. */
237
+ setRequestGuard(guard) {
238
+ this.requestGuard = guard;
239
+ }
229
240
  setPromptFn(fn) {
230
241
  this.promptFn = fn;
231
242
  }
@@ -313,13 +324,20 @@ export class InteractiveApprovalBackend {
313
324
  this.sessionStateById.get(sessionId) === state);
314
325
  }
315
326
  async requestApproval(req) {
327
+ const unavailable = this.requestGuard?.(req);
328
+ if (unavailable)
329
+ return unavailable;
316
330
  const state = this.getSessionState(req.sessionId, true);
317
331
  // Fast path: the operation may already be covered by a session rule.
318
332
  const cached = state ? this.checkSessionRules(req, state) : null;
319
333
  if (cached)
320
334
  return cached;
321
335
  if (!this.promptFn && !this.approvalRouter.hasConnections()) {
322
- return { approved: false, reason: "interactive approval backend has no prompt function" };
336
+ return {
337
+ approved: false,
338
+ failure: "unavailable",
339
+ reason: "interactive approval backend has no prompt function",
340
+ };
323
341
  }
324
342
  // Serialize prompts and re-check rules when our turn comes (see promptTurn
325
343
  // field doc): a burst of parallel tool calls all passes the fast path
@@ -336,6 +354,9 @@ export class InteractiveApprovalBackend {
336
354
  }
337
355
  try {
338
356
  await prevTurn;
357
+ const unavailable = this.requestGuard?.(req);
358
+ if (unavailable)
359
+ return unavailable;
339
360
  const activeState = this.isActiveSessionState(req.sessionId, state) ? state : null;
340
361
  const nowCached = activeState ? this.checkSessionRules(req, activeState) : null;
341
362
  if (nowCached)
@@ -374,12 +395,20 @@ export class InteractiveApprovalBackend {
374
395
  ? await fallback
375
396
  : {
376
397
  approved: false,
398
+ failure: "owner_lost",
377
399
  reason: `no approval connection owns session ${req.sessionId}`,
378
400
  };
379
401
  }
380
402
  else {
381
- return { approved: false, reason: "interactive approval backend has no prompt function" };
403
+ return {
404
+ approved: false,
405
+ failure: "unavailable",
406
+ reason: "interactive approval backend has no prompt function",
407
+ };
382
408
  }
409
+ const unavailable = this.requestGuard?.(req);
410
+ if (unavailable)
411
+ return unavailable;
383
412
  const scope = result.scope ?? (result.always ? "session" : "once");
384
413
  const activeState = this.isActiveSessionState(req.sessionId, state) ? state : null;
385
414
  const context = activeState ?? (!req.sessionId ? this.legacyContext : null);
@@ -1322,13 +1351,20 @@ export class PermissionClassifier {
1322
1351
  }
1323
1352
  }
1324
1353
  async handleAsk(toolName, args, reason, opts) {
1354
+ return (await this.handleAskResult(toolName, args, reason, opts)).approved;
1355
+ }
1356
+ async handleAskResult(toolName, args, reason, opts) {
1325
1357
  if (this.defaultMode === "dontAsk") {
1326
1358
  this.log.info("permission.auto_deny", {
1327
1359
  cat: "permission",
1328
1360
  tool: toolName,
1329
1361
  reason: "dontAsk_mode",
1330
1362
  });
1331
- return false;
1363
+ return {
1364
+ approved: false,
1365
+ failure: "policy_denied",
1366
+ reason: "permission policy does not allow prompting",
1367
+ };
1332
1368
  }
1333
1369
  if (this.defaultMode === "bypassPermissions") {
1334
1370
  this.log.info("permission.auto_allow", {
@@ -1336,7 +1372,7 @@ export class PermissionClassifier {
1336
1372
  tool: toolName,
1337
1373
  reason: "bypassPermissions_mode",
1338
1374
  });
1339
- return true;
1375
+ return { approved: true };
1340
1376
  }
1341
1377
  // Check denial tracker — if too many denials, auto-deny
1342
1378
  if (this.denialTracker.shouldWarn(toolName)) {
@@ -1345,7 +1381,7 @@ export class PermissionClassifier {
1345
1381
  tool: toolName,
1346
1382
  reason: "denial_tracker_threshold",
1347
1383
  });
1348
- return false;
1384
+ return { approved: false, failure: "policy_denied", reason: "repeated user denials" };
1349
1385
  }
1350
1386
  const riskLevel = this.assessRisk(toolName, args);
1351
1387
  const span = this.log.span("permission.ask", {
@@ -1385,7 +1421,7 @@ export class PermissionClassifier {
1385
1421
  if (result.approved) {
1386
1422
  this.denialTracker.recordSuccess(toolName);
1387
1423
  }
1388
- else {
1424
+ else if (!result.failure || result.failure === "denied") {
1389
1425
  this.denialTracker.record(toolName);
1390
1426
  }
1391
1427
  this.emitApprovalEvent({
@@ -1403,7 +1439,7 @@ export class PermissionClassifier {
1403
1439
  scope: result.scope,
1404
1440
  reason: !result.approved ? result.reason : undefined,
1405
1441
  });
1406
- return result.approved;
1442
+ return result;
1407
1443
  }
1408
1444
  /** Get denial warning message if the model keeps getting denied. */
1409
1445
  getDenialWarning(toolName) {
@@ -35,6 +35,7 @@ export const READ_ONLY_TOOLS = new Set([
35
35
  "WebSearch",
36
36
  "WebFetch",
37
37
  "ToolSearch",
38
+ "SearchHistory",
38
39
  ]);
39
40
  export const PLAN_MODE_ALLOWED_TOOLS = new Set([
40
41
  // Plan lifecycle
@@ -60,6 +61,10 @@ export const PLAN_MODE_ALLOWED_TOOLS = new Set([
60
61
  "TaskUpdate",
61
62
  "TaskList",
62
63
  "TaskGet",
64
+ // Session-local working state remains available across planning contexts.
65
+ "SaveContextNote",
66
+ "NewContext",
67
+ "SearchHistory",
63
68
  // Bash: visible to the model; executor gates it to read-only commands.
64
69
  "Bash",
65
70
  ]);
@@ -14,9 +14,43 @@
14
14
  * sandbox-exec is technically deprecated by Apple but remains the only
15
15
  * working OS-level sandbox on macOS and is what Codex CLI / Cursor use today.
16
16
  */
17
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { execFileSync } from "node:child_process";
18
+ import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
18
19
  import { tmpdir } from "node:os";
19
20
  import { join } from "node:path";
21
+ /**
22
+ * Per-user MDS (Module Directory Service) scratch directory.
23
+ *
24
+ * Security.framework opens the keychain through MDS, which needs to write a
25
+ * lock file under the per-user Darwin cache dir. Without it *any* keychain
26
+ * read fails — see the `(allow file-write* …/mds)` clause in buildProfile()
27
+ * for why that matters and how it presents.
28
+ *
29
+ * `getconf DARWIN_USER_CACHE_DIR` reports the `/var/folders/…` form, but
30
+ * Seatbelt matches subpaths canonically (`/private/var/folders/…`), so the
31
+ * result is realpath'd — the same footgun expandConfig() handles for
32
+ * writableRoots. Resolved once per process; the value is stable for a user.
33
+ */
34
+ let mdsCacheDir;
35
+ function resolveMdsCacheDir() {
36
+ if (mdsCacheDir !== undefined)
37
+ return mdsCacheDir;
38
+ try {
39
+ const cacheDir = execFileSync("/usr/bin/getconf", ["DARWIN_USER_CACHE_DIR"], {
40
+ encoding: "utf-8",
41
+ timeout: 5_000,
42
+ }).trim();
43
+ // Bail rather than emit a bogus rule if getconf returns something odd.
44
+ mdsCacheDir = cacheDir ? realpathSync(join(cacheDir, "mds")) : null;
45
+ }
46
+ catch {
47
+ // getconf missing/failed, or no mds dir yet on this host. Keychain reads
48
+ // stay broken, but that's strictly the pre-existing behavior — never fail
49
+ // the whole sandbox over it.
50
+ mdsCacheDir = null;
51
+ }
52
+ return mdsCacheDir;
53
+ }
20
54
  export function createSeatbeltBackend(config) {
21
55
  return {
22
56
  name: "seatbelt",
@@ -44,6 +78,20 @@ export function createSeatbeltBackend(config) {
44
78
  };
45
79
  },
46
80
  hintForBlockedOutput(stderr) {
81
+ // Keychain denials never say "sandbox" or "Operation not permitted" —
82
+ // they surface as an MDS error, or as a downstream tool blaming its own
83
+ // credentials ("token invalid") after silently falling back off the
84
+ // keyring. Match the MDS string specifically so the model stops
85
+ // recommending a re-login for what is a sandbox problem. This is a
86
+ // deterministic error string from Security.framework, not prose.
87
+ if (/Module Directory Service error/.test(stderr)) {
88
+ return ("\n[sandbox:seatbelt] A keychain read failed inside the sandbox " +
89
+ "(Security.framework/MDS). Any 'invalid token' / 'not logged in' " +
90
+ "message above is likely bogus — the credential is in the macOS " +
91
+ "keychain and was unreadable, NOT wrong. Do not re-run `auth login`. " +
92
+ "If this host has an unusual DARWIN_USER_CACHE_DIR, ask the user to " +
93
+ "add its `mds` dir to sandbox.writableRoots in settings.json.");
94
+ }
47
95
  if (/Operation not permitted|sandbox/.test(stderr)) {
48
96
  return ("\n[sandbox:seatbelt] A syscall was blocked by the sandbox. " +
49
97
  "If this path should be writable or this network call legitimate, " +
@@ -57,6 +105,27 @@ function buildProfile(config) {
57
105
  const writeAllows = config.writableRoots.map((p) => ` (subpath ${quote(p)})`).join("\n");
58
106
  const readDenies = config.deniedReads.map((p) => ` (subpath ${quote(p)})`).join("\n");
59
107
  const networkClause = config.network === "deny" ? "(deny network-outbound)" : "(allow network*)";
108
+ // Keychain access. Tools that store credentials in the macOS keychain
109
+ // (`gh`, `az`, `docker login`, anything calling /usr/bin/security) shell
110
+ // out to Security.framework, which reaches the keychain via MDS — and MDS
111
+ // needs to write a lock file under the per-user Darwin cache dir. That dir
112
+ // is outside the workspace, so a write-restricted profile blocks it.
113
+ //
114
+ // The failure is worth spelling out because it does NOT look like a
115
+ // sandbox denial: `security` exits 44 with "A Module Directory Service
116
+ // error has occurred", and callers treat that as "no keychain" and fall
117
+ // back to their plaintext config. `gh` then reports "The token in default
118
+ // is invalid" — pointing at the token, which is fine, instead of at the
119
+ // sandbox. Users burn a lot of time re-running `gh auth login` here.
120
+ //
121
+ // Verified empirically: this single clause is the minimal delta that makes
122
+ // `gh auth status` report `(keyring)` instead of `(default)` under an
123
+ // otherwise unchanged profile. Reads of ~/.ssh and writes outside the
124
+ // workspace stay denied — the grant is one scratch dir, not a hole.
125
+ const mdsDir = resolveMdsCacheDir();
126
+ const keychainClause = mdsDir
127
+ ? `\n;; Keychain (Security.framework/MDS scratch)\n(allow file-write* (subpath ${quote(mdsDir)}))\n`
128
+ : "";
60
129
  // SBPL evaluation note: when a broad `(allow file-read*)` and a specific
61
130
  // `(deny file-read* (subpath …))` both match, the more specific subpath
62
131
  // rule wins — order between the two clauses does not matter. We tested
@@ -88,7 +157,7 @@ ${writeAllows})
88
157
  (literal "/dev/random")
89
158
  (literal "/dev/urandom")
90
159
  (literal "/dev/dtracehelper"))
91
-
160
+ ${keychainClause}
92
161
  ;; IPC & system services common tools need
93
162
  (allow mach-lookup)
94
163
  (allow ipc-posix-shm)
@@ -87,9 +87,17 @@ export function createSessionToolHost(options) {
87
87
  ...options.contextOverrides,
88
88
  sessionId: options.businessSessionId,
89
89
  externalRuntime: true,
90
+ // This bridge owns tool calls, not a native model loop that can apply a
91
+ // context rollover. Never expose native working-memory controls here.
92
+ contextStrategy: undefined,
93
+ contextNotes: undefined,
90
94
  planMode: options.planMode,
91
95
  permissionMode: options.permissionMode,
92
- toolVisibility: buildToolVisibility(options.visibility),
96
+ toolVisibility: buildToolVisibility({
97
+ ...options.visibility,
98
+ contextStrategy: undefined,
99
+ hasBrowserAutomation: options.contextOverrides?.browser !== undefined,
100
+ }),
93
101
  // Belt and braces with the exposure check in execute(): the executor
94
102
  // enforces this too, so a future refactor that loses one still fails closed.
95
103
  allowedToolNames: new Set(exposure.toolNames),
package/dist/types.d.ts CHANGED
@@ -145,7 +145,7 @@ export interface RegisteredTool {
145
145
  */
146
146
  timeoutMs?: number;
147
147
  }
148
- export type TranscriptEventType = "message" | "tool_use" | "tool_result" | "summary" | "context_transfer" | "range_archive" | "content_replace" | "file_history" | "plan_operation" | "session_meta" | "subagent" | "external_file_changes" | "turn_boundary" | "run_result" | "goal_progress" | "turn_stopped" | "error";
148
+ export type TranscriptEventType = "message" | "tool_use" | "tool_result" | "summary" | "context_transfer" | "context_note" | "context_checkpoint" | "range_archive" | "content_replace" | "file_history" | "plan_operation" | "session_meta" | "subagent" | "external_file_changes" | "turn_boundary" | "run_result" | "goal_progress" | "turn_stopped" | "error";
149
149
  export interface TranscriptEvent {
150
150
  id: string;
151
151
  type: TranscriptEventType;
@@ -423,6 +423,8 @@ export type ApprovalResult = {
423
423
  } | {
424
424
  approved: false;
425
425
  reason?: string;
426
+ /** A host/policy failure is distinct from an explicit user denial. */
427
+ failure?: "denied" | "cancelled" | "session_closed" | "owner_lost" | "timed_out" | "unavailable" | "policy_denied";
426
428
  always?: boolean;
427
429
  scope?: ApprovalScope;
428
430
  };
@@ -586,7 +588,7 @@ export type StreamEvent = {
586
588
  agentId?: string;
587
589
  } | {
588
590
  type: "context_compact";
589
- strategy: "micro" | "summary" | "window" | "snip" | "emergency" | "compacted" | "range";
591
+ strategy: "micro" | "summary" | "window" | "snip" | "emergency" | "compacted" | "range" | "notes";
590
592
  before: number;
591
593
  after: number;
592
594
  agentId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.9.5",
3
+ "version": "0.9.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",