@yanlinglabs/winter-agent-sdk 0.0.17 → 0.0.22

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.
@@ -118,7 +118,7 @@ function resolveWinterHome2(env, brand) {
118
118
  return override;
119
119
  const profile = e[envName2(b, "PROFILE")];
120
120
  const dirName = profile !== undefined && profile.trim() === "dev" ? `${b.homeDirName}-dev` : b.homeDirName;
121
- return join(homedir(), dirName);
121
+ return join(homedir(), dirName, "sdk");
122
122
  }
123
123
  function resolveKeychainServiceForProfile2(brand, env, hostSetKeychainService) {
124
124
  if (hostSetKeychainService)
package/dist/index.d.ts CHANGED
@@ -27,11 +27,13 @@ export { SDK_VERSION } from "./version.js";
27
27
  export { MESSAGING_CONTROL_SUBTYPES, MESSAGING_CONTROL_SUBTYPE_LIST, MESSAGING_HOST_REQUEST_SUBTYPES, MESSAGING_RUNTIME_REQUEST_SUBTYPES, resolveFacetTarget } from "./protocol/messaging.js";
28
28
  export { isRuntimeAddress, isGlobalAgentMessage, isDeliveryOutcome, isListedRuntimeObjectArray, isPermissionClassLabel, isMessagingDeliverRequest, isMessagingChildRequest, isMessagingSubscribeIdleRequest, isMessagingReadNotificationsRequest, isMessagingNotificationsPage, isMessagingIdleNoticePayload, isNotificationRecord, } from "./protocol/messaging.js";
29
29
  export type { MessagingControlSubtype, MessagingDeliverRequest, MessagingChildRequest, MessagingSubscribeIdleRequest, MessagingSenderClassResponse, MessagingReadNotificationsRequest, MessagingNotificationsPage, MessagingIdleNoticePayload, } from "./protocol/messaging.js";
30
- export type { ProtocolVersion, WinterFrame, SdkMessage as ProtocolSdkMessage, InitFrame, UserFrame, DataFrame, ControlRequestFrame, ControlResponseFrame, UnknownFrame, SDKHookStartedMessage, SDKHookProgressMessage, SDKHookResponseMessage, SDKPermissionDeniedMessage, SDKPermissionDenial, SDKTaskStartedMessage, SDKTaskNotificationMessage, SDKTaskUpdatedMessage, SDKTaskProgressMessage, SDKBackgroundTasksChangedMessage, SDKLocalCommandOutputMessage, BackgroundTaskMessage, SDKCompactBoundaryMessage, SDKSessionStateChangedMessage, WireContentBlock, WireStreamEvent, WireStreamEventDelta, SDKPartialAssistantMessage, SDKAssistantMessageError, SDKAPIRetryMessage, SDKRateLimitEvent, SDKRateLimitInfo, SDKAuthStatusMessage, SDKThinkingTokensMessage, SDKModelRefusalFallbackMessage, SDKModelRefusalNoFallbackMessage, SDKReasoningSummaryMessage, SDKModelSwitchMessage, SDKContinuityWarningMessage, } from "./protocol/frames.js";
30
+ export type { ProtocolVersion, WinterFrame, SdkMessage as ProtocolSdkMessage, InitFrame, UserFrame, DataFrame, ControlRequestFrame, ControlResponseFrame, ControlCancelRequestFrame, WireResultUsage, UnknownFrame, SDKHookStartedMessage, SDKHookProgressMessage, SDKHookResponseMessage, SDKPermissionDeniedMessage, SDKPermissionDenial, SDKTaskStartedMessage, SDKTaskNotificationMessage, SDKTaskUpdatedMessage, SDKTaskProgressMessage, SDKBackgroundTasksChangedMessage, SDKLocalCommandOutputMessage, BackgroundTaskMessage, SDKCompactBoundaryMessage, SDKSessionStateChangedMessage, WireContentBlock, WireStreamEvent, WireStreamEventDelta, SDKPartialAssistantMessage, SDKAssistantMessageError, SDKAPIRetryMessage, SDKRateLimitEvent, SDKRateLimitInfo, SDKAuthStatusMessage, SDKThinkingTokensMessage, SDKModelRefusalFallbackMessage, SDKModelRefusalNoFallbackMessage, SDKReasoningSummaryMessage, SDKModelSwitchMessage, SDKContinuityWarningMessage, } from "./protocol/frames.js";
31
31
  export { resolveWinterHome, resolveKeychainServiceForProfile, isUnset } from "./paths/home.js";
32
32
  export { transcriptProjectKey, TRANSCRIPT_PROJECT_KEY_MAX_LENGTH, isVendorCompliantProjectKey } from "./paths/project-key.js";
33
33
  export { compatibilityKeys } from "./paths/keys.js";
34
34
  export type { CompatibilityKeys } from "./paths/keys.js";
35
+ export { globalConfigFileName } from "./paths/global-config.js";
36
+ export { storeHomeEnvName, pluginCacheDirEnvName, providerManagedByHostEnvName, disableCronEnvName } from "./paths/host-env.js";
35
37
  export { WinterCompatibilitySessionStore, WinterStoreError, WinterStoreLeaseError, DIALECT_RECORD_ENTRY_TYPE, PROVIDER_STATE_FILE_SUFFIX, } from "./store/session-store.js";
36
38
  export type { SessionKey, SessionStoreEntry, SessionSummaryEntry, SessionStore } from "./store/session-store.js";
37
39
  export { forkSessionByKey } from "./store/fork-session.js";
@@ -49,3 +51,7 @@ export type { PermissionUpdate, PermissionUpdateDestination, RuleSource } from "
49
51
  export type { CanUseTool, PermissionResult, PermissionDecisionClassification, PermissionRequestPayload } from "./permissions/types.js";
50
52
  export { HOOK_EVENTS } from "./permissions/types.js";
51
53
  export type { HookEvent, HookEventName, HookSource, HookPermissionDecision, BaseHookInput, PreToolUseHookInput, PostToolUseHookInput, PostToolUseFailureHookInput, UserPromptSubmitHookInput, StopHookInput, SessionStartHookInput, SessionEndHookInput, NotificationHookInput, PermissionRequestHookInput, PermissionDeniedHookInput, GenericHookInput, HookInput, AsyncHookJSONOutput, SyncHookJSONOutput, HookJSONOutput, PreToolUseHookSpecificOutput, PostToolUseHookSpecificOutput, PostToolUseFailureHookSpecificOutput, UserPromptSubmitHookSpecificOutput, StopHookSpecificOutput, SessionStartHookSpecificOutput, NotificationHookSpecificOutput, PermissionRequestHookSpecificOutput, PermissionDeniedHookSpecificOutput, GenericHookSpecificOutput, HookSpecificOutput, HookCallback, HookCallbackMatcher, HookInvocationPayload, } from "./permissions/types.js";
54
+ export { CATALOG_TAG_RENAMES } from "@yanlinglabs/winter-provider-catalog";
55
+ export { PluginManagerError, listMarketplaces, addMarketplace, removeMarketplace, updateMarketplace, installPlugin, uninstallPlugin, setPluginEnabled, updatePlugin, listPlugins } from "./plugins/manage.js";
56
+ export type { PluginScope, PluginManagerOptions, MarketplaceInfo, InstalledPlugin, PluginListing } from "./plugins/manage.js";
57
+ export { resolveMarketplacePluginPath } from "./plugins/marketplace-path.js";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  DIALECT_RECORD_ENTRY_TYPE2,
15
15
  PROVIDER_STATE_FILE_SUFFIX2,
16
16
  WinterCompatibilitySessionStore2
17
- } from "./index-qst9rwbb.js";
17
+ } from "./index-0xks1t4r.js";
18
18
  import {
19
19
  deliveryUncertain2,
20
20
  refused2,
@@ -666,6 +666,7 @@ function query(args) {
666
666
  proc.stdin.write(encodeFrame(frame));
667
667
  }
668
668
  const controlRequestHandlers = new Map;
669
+ const incomingRequestAborts = new Map;
669
670
  async function handleIncomingControlRequest(cf) {
670
671
  try {
671
672
  const handler = controlRequestHandlers.get(cf.subtype);
@@ -678,8 +679,12 @@ function query(args) {
678
679
  });
679
680
  return;
680
681
  }
682
+ const cancel = new AbortController;
683
+ incomingRequestAborts.set(cf.requestId, cancel);
681
684
  try {
682
- const result = await handler(cf.payload);
685
+ const result = await handler(cf.payload, { signal: cancel.signal });
686
+ if (cancel.signal.aborted)
687
+ return;
683
688
  if (result.ok) {
684
689
  writeControlResponse({
685
690
  type: "control_response",
@@ -691,19 +696,27 @@ function query(args) {
691
696
  writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: result.error });
692
697
  }
693
698
  } catch (err) {
699
+ if (cancel.signal.aborted)
700
+ return;
694
701
  const message = err instanceof Error ? err.message : String(err);
695
702
  writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: { code: "handler_threw", message } });
703
+ } finally {
704
+ incomingRequestAborts.delete(cf.requestId);
696
705
  }
697
706
  } catch {}
698
707
  }
699
708
  function makePermissionHandler(canUseTool) {
700
- return async (payload) => {
709
+ return async (payload, handlerCtx) => {
701
710
  const req = payload;
702
711
  const controller = new AbortController;
703
712
  if (options.abortController?.signal.aborted)
704
713
  controller.abort();
705
714
  else
706
715
  options.abortController?.signal.addEventListener("abort", () => controller.abort(), { once: true });
716
+ if (handlerCtx?.signal.aborted === true)
717
+ controller.abort();
718
+ else
719
+ handlerCtx?.signal.addEventListener("abort", () => controller.abort(), { once: true });
707
720
  let result;
708
721
  try {
709
722
  result = await canUseTool(req.toolName, req.input, {
@@ -828,6 +841,13 @@ function query(args) {
828
841
  }
829
842
  continue;
830
843
  }
844
+ if (frame.type === "control_cancel_request") {
845
+ const cancelledId = frame.requestId ?? "";
846
+ const cancelled = incomingRequestAborts.get(cancelledId);
847
+ incomingRequestAborts.delete(cancelledId);
848
+ cancelled?.abort();
849
+ continue;
850
+ }
831
851
  if (frame.type === "control_request") {
832
852
  handleIncomingControlRequest(frame);
833
853
  continue;
@@ -1001,7 +1021,7 @@ function query(args) {
1001
1021
  return gen;
1002
1022
  }
1003
1023
  // src/version.ts
1004
- var SDK_VERSION = "0.0.17";
1024
+ var SDK_VERSION = "0.0.22";
1005
1025
  // src/paths/project-key.ts
1006
1026
  var TRANSCRIPT_PROJECT_KEY_MAX_LENGTH = 64;
1007
1027
  var VENDOR_PROJECT_KEY_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
@@ -1044,13 +1064,41 @@ function resolveCanonical(raw) {
1044
1064
  }
1045
1065
  }
1046
1066
  function gitCommonRoot(resolvedCwd) {
1067
+ let commonDir;
1068
+ try {
1069
+ const raw = runGit(["-C", resolvedCwd, "rev-parse", "--git-common-dir"]);
1070
+ commonDir = platformNormalize(realpathSync(isAbsolute(raw) ? raw : resolvePath(resolvedCwd, raw)));
1071
+ } catch {
1072
+ return null;
1073
+ }
1074
+ const candidate = dirname2(commonDir);
1075
+ if (isWithin(resolvedCwd, candidate))
1076
+ return candidate;
1077
+ if (registeredWorktrees(commonDir).some((worktree) => isWithin(resolvedCwd, worktree)))
1078
+ return candidate;
1079
+ const configured = configuredWorktree(commonDir);
1080
+ if (configured !== null && isWithin(resolvedCwd, configured))
1081
+ return configured;
1082
+ return null;
1083
+ }
1084
+ function runGit(args) {
1085
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1086
+ }
1087
+ function isWithin(path, dir) {
1088
+ return path === dir || path.startsWith(dir.endsWith("/") ? dir : `${dir}/`);
1089
+ }
1090
+ function registeredWorktrees(commonDir) {
1091
+ try {
1092
+ return runGit(["--git-dir", commonDir, "worktree", "list", "--porcelain"]).split(`
1093
+ `).filter((line) => line.startsWith("worktree ")).map((line) => resolveCanonical(line.slice("worktree ".length)));
1094
+ } catch {
1095
+ return [];
1096
+ }
1097
+ }
1098
+ function configuredWorktree(commonDir) {
1047
1099
  try {
1048
- const raw = execFileSync("git", ["-C", resolvedCwd, "rev-parse", "--git-common-dir"], {
1049
- encoding: "utf8",
1050
- stdio: ["ignore", "pipe", "ignore"]
1051
- }).trim();
1052
- const commonDir = isAbsolute(raw) ? raw : resolvePath(resolvedCwd, raw);
1053
- return platformNormalize(dirname2(realpathSync(commonDir)));
1100
+ const raw = runGit(["--git-dir", commonDir, "config", "--get", "core.worktree"]);
1101
+ return raw.length > 0 ? resolveCanonical(isAbsolute(raw) ? raw : resolvePath(commonDir, raw)) : null;
1054
1102
  } catch {
1055
1103
  return null;
1056
1104
  }
@@ -1062,6 +1110,23 @@ function compatibilityKeys(cwd) {
1062
1110
  const memoryKey = commonRoot === null ? cwdKey : transcriptProjectKey(commonRoot);
1063
1111
  return { transcriptProjectKey: cwdKey, memoryProjectKey: memoryKey, tempProjectKey: cwdKey };
1064
1112
  }
1113
+ // src/paths/global-config.ts
1114
+ function globalConfigFileName(brand) {
1115
+ return `${brand.homeDirName}.json`;
1116
+ }
1117
+ // src/paths/host-env.ts
1118
+ function storeHomeEnvName(brand) {
1119
+ return envName2(brand, "STORE_HOME");
1120
+ }
1121
+ function pluginCacheDirEnvName(brand) {
1122
+ return envName2(brand, "PLUGIN_CACHE_DIR");
1123
+ }
1124
+ function providerManagedByHostEnvName(brand) {
1125
+ return envName2(brand, "PROVIDER_MANAGED_BY_HOST");
1126
+ }
1127
+ function disableCronEnvName(brand) {
1128
+ return envName2(brand, "DISABLE_CRON");
1129
+ }
1065
1130
  // src/store/fork-session.ts
1066
1131
  import { randomUUID as randomUUID2 } from "node:crypto";
1067
1132
  var IDENTITY_FIELDS = ["providerId", "modelKey", "adapterId", "adapterVersion", "catalogVersion", "authRef", "classifierPin"];
@@ -1394,6 +1459,45 @@ function unionRuleArray(tiersLowestFirst, key) {
1394
1459
  }
1395
1460
  return out.length > 0 ? out : undefined;
1396
1461
  }
1462
+ function unionSandboxFilesystemArray(tiersLowestFirst, key) {
1463
+ const seen = new Set;
1464
+ const out = [];
1465
+ for (const tier of tiersLowestFirst) {
1466
+ const sandbox = tier.values["sandbox"];
1467
+ if (!isPlainObject2(sandbox))
1468
+ continue;
1469
+ const filesystem = sandbox["filesystem"];
1470
+ if (!isPlainObject2(filesystem))
1471
+ continue;
1472
+ for (const rule of stringArrayOrUndefined(filesystem[key]) ?? []) {
1473
+ if (seen.has(rule))
1474
+ continue;
1475
+ seen.add(rule);
1476
+ out.push(rule);
1477
+ }
1478
+ }
1479
+ return out.length > 0 ? out : undefined;
1480
+ }
1481
+ function unionSandboxFilesystemDenyArrays(effective, tiersLowestFirst) {
1482
+ const mergedSandbox = effective["sandbox"];
1483
+ const mergedFilesystem = isPlainObject2(mergedSandbox) ? mergedSandbox["filesystem"] : undefined;
1484
+ const filesystem = isPlainObject2(mergedFilesystem) ? { ...mergedFilesystem } : {};
1485
+ let any = isPlainObject2(mergedFilesystem);
1486
+ for (const key of ["allowWrite", "denyWrite", "allowRead", "denyRead"]) {
1487
+ const unioned = unionSandboxFilesystemArray(tiersLowestFirst, key);
1488
+ if (unioned === undefined)
1489
+ delete filesystem[key];
1490
+ else {
1491
+ filesystem[key] = unioned;
1492
+ any = true;
1493
+ }
1494
+ }
1495
+ if (!any)
1496
+ return;
1497
+ const sandbox = isPlainObject2(mergedSandbox) ? { ...mergedSandbox } : {};
1498
+ sandbox["filesystem"] = filesystem;
1499
+ effective["sandbox"] = sandbox;
1500
+ }
1397
1501
  function restrictProviderEnables(effective, tiersLowestFirst) {
1398
1502
  const disabled = new Set;
1399
1503
  for (const tier of tiersLowestFirst) {
@@ -1492,6 +1596,7 @@ async function resolveSettingsDetailed(opts = {}) {
1492
1596
  const overlayFilteredTiers = lowestFirst.map((entry) => ({ values: tierContribution(entry.values, entry.source === "project", trustedWorkspace) }));
1493
1597
  unionPermissionRuleArrays(effective, overlayFilteredTiers);
1494
1598
  restrictProviderEnables(effective, overlayFilteredTiers);
1599
+ unionSandboxFilesystemDenyArrays(effective, overlayFilteredTiers);
1495
1600
  if (!trustedWorkspace) {
1496
1601
  const projectIndex = lowestFirst.findIndex((e) => e.source === "project");
1497
1602
  if (projectIndex !== -1) {
@@ -1692,9 +1797,394 @@ var HOOK_EVENTS = [
1692
1797
  "FileChanged",
1693
1798
  "DirectoryAdded"
1694
1799
  ];
1800
+
1801
+ // src/index.ts
1802
+ import { CATALOG_TAG_RENAMES } from "@yanlinglabs/winter-provider-catalog";
1803
+
1804
+ // src/plugins/manage.ts
1805
+ import { mkdir, open, readFile as readFile2, rename, rmdir, unlink, writeFile } from "node:fs/promises";
1806
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
1807
+ import { randomBytes } from "node:crypto";
1808
+
1809
+ // src/plugins/marketplace-path.ts
1810
+ import { resolve, sep } from "node:path";
1811
+ var BARE_SOURCE_NAME = /^[A-Za-z0-9][-A-Za-z0-9._]*$/;
1812
+ function isBareSourceName(source) {
1813
+ return BARE_SOURCE_NAME.test(source) && !source.includes("..");
1814
+ }
1815
+ function normalizePluginRoot(pluginRoot) {
1816
+ if (typeof pluginRoot !== "string" || pluginRoot === "" || pluginRoot.startsWith("/") || pluginRoot.includes("\\") || pluginRoot.includes(":"))
1817
+ return;
1818
+ const stripped = pluginRoot.replace(/^\.\//, "").replace(/\/+$/, "");
1819
+ if (stripped === "" || stripped === ".")
1820
+ return ".";
1821
+ if (stripped.split("/").some((segment) => segment === "" || segment === "." || segment === ".."))
1822
+ return;
1823
+ return stripped;
1824
+ }
1825
+ function withPluginRootPrefix(source, normalizedRoot) {
1826
+ if (normalizedRoot === undefined || !isBareSourceName(source))
1827
+ return source;
1828
+ return normalizedRoot === "." ? `./${source}` : `./${normalizedRoot}/${source}`;
1829
+ }
1830
+ function schemaSourcePath(source) {
1831
+ const normalized = source === "." ? "./" : source;
1832
+ return normalized.startsWith("./") ? normalized : undefined;
1833
+ }
1834
+ function resolveWithinBase(base, relativePath) {
1835
+ const normalizedBase = resolve(base);
1836
+ const resolved = resolve(base, relativePath);
1837
+ if (resolved !== normalizedBase && !resolved.startsWith(normalizedBase + sep)) {
1838
+ throw new Error(`Path traversal detected: "${relativePath}" would escape the base directory`);
1839
+ }
1840
+ return resolved;
1841
+ }
1842
+ function resolveMarketplacePluginPath(installLocation, pluginRoot, source) {
1843
+ if (typeof source !== "string" || source === "")
1844
+ return;
1845
+ const normalizedRoot = normalizePluginRoot(pluginRoot);
1846
+ const prefixed = withPluginRootPrefix(source, normalizedRoot);
1847
+ const sourcePath = schemaSourcePath(prefixed);
1848
+ if (sourcePath === undefined)
1849
+ return;
1850
+ try {
1851
+ return resolveWithinBase(installLocation, sourcePath);
1852
+ } catch {
1853
+ return;
1854
+ }
1855
+ }
1856
+
1857
+ // src/plugins/manage.ts
1858
+ class PluginManagerError extends Error {
1859
+ constructor(message) {
1860
+ super(message);
1861
+ this.name = "PluginManagerError";
1862
+ }
1863
+ }
1864
+ function installedPluginsPath(o) {
1865
+ return join3(o.pluginsRoot, "installed_plugins.json");
1866
+ }
1867
+ function knownMarketplacesPath(o) {
1868
+ return join3(o.pluginsRoot, "known_marketplaces.json");
1869
+ }
1870
+ function isEnoent(err) {
1871
+ return err?.code === "ENOENT";
1872
+ }
1873
+ var RETRYABLE_RENAME_CODES = new Set(["EXDEV", "EPERM", "EEXIST", "EBUSY"]);
1874
+ async function writeJsonAtomicNoLock(path, data) {
1875
+ const body = `${JSON.stringify(data, null, 2)}
1876
+ `;
1877
+ const tmp = `${path}.tmp.${randomBytes(4).toString("hex")}`;
1878
+ let created = false;
1879
+ try {
1880
+ const handle = await open(tmp, "wx", 384);
1881
+ created = true;
1882
+ try {
1883
+ await handle.writeFile(body, "utf8");
1884
+ } finally {
1885
+ await handle.close();
1886
+ }
1887
+ await rename(tmp, path);
1888
+ } catch (err) {
1889
+ const code = err?.code;
1890
+ if (code === undefined || !RETRYABLE_RENAME_CODES.has(code))
1891
+ throw err;
1892
+ await writeFile(path, body, { encoding: "utf8", mode: 384 });
1893
+ if (created) {
1894
+ try {
1895
+ await unlink(tmp);
1896
+ } catch (cleanupErr) {
1897
+ if (!isEnoent(cleanupErr))
1898
+ throw cleanupErr;
1899
+ }
1900
+ }
1901
+ }
1902
+ }
1903
+ async function readInstalledPluginsFile(o) {
1904
+ try {
1905
+ const raw = await readFile2(installedPluginsPath(o), "utf8");
1906
+ const parsed = JSON.parse(raw);
1907
+ const plugins = parsed !== null && typeof parsed === "object" && typeof parsed.plugins === "object" && parsed.plugins !== null ? parsed.plugins : {};
1908
+ return { version: 2, plugins };
1909
+ } catch (err) {
1910
+ if (isEnoent(err))
1911
+ return { version: 2, plugins: {} };
1912
+ throw err;
1913
+ }
1914
+ }
1915
+ var MARKETPLACES_LOCK_ATTEMPTS = 25;
1916
+ var MARKETPLACES_LOCK_RETRY_DELAY_MS = 20;
1917
+ async function withMarketplacesLock(path, fn) {
1918
+ const lockPath = `${path}.lock`;
1919
+ let acquired = false;
1920
+ for (let attempt = 0;attempt < MARKETPLACES_LOCK_ATTEMPTS; attempt++) {
1921
+ try {
1922
+ await mkdir(lockPath);
1923
+ acquired = true;
1924
+ break;
1925
+ } catch (err) {
1926
+ if (err?.code !== "EEXIST")
1927
+ throw err;
1928
+ await new Promise((r) => setTimeout(r, MARKETPLACES_LOCK_RETRY_DELAY_MS));
1929
+ }
1930
+ }
1931
+ try {
1932
+ return await fn();
1933
+ } finally {
1934
+ if (acquired) {
1935
+ try {
1936
+ await rmdir(lockPath);
1937
+ } catch (err) {
1938
+ if (!isEnoent(err))
1939
+ throw err;
1940
+ }
1941
+ }
1942
+ }
1943
+ }
1944
+ async function readKnownMarketplacesFile(o) {
1945
+ try {
1946
+ const raw = await readFile2(knownMarketplacesPath(o), "utf8");
1947
+ const parsed = JSON.parse(raw);
1948
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1949
+ } catch (err) {
1950
+ if (isEnoent(err))
1951
+ return {};
1952
+ throw err;
1953
+ }
1954
+ }
1955
+ async function writeKnownMarketplacesFile(path, file) {
1956
+ await writeJsonAtomicNoLock(path, file);
1957
+ }
1958
+ async function readDirectoryMarketplaceManifest(marketplaceDir) {
1959
+ const manifestPath = join3(marketplaceDir, ".claude-plugin", "marketplace.json");
1960
+ let raw;
1961
+ try {
1962
+ raw = await readFile2(manifestPath, "utf8");
1963
+ } catch (err) {
1964
+ if (isEnoent(err))
1965
+ throw new PluginManagerError(`${manifestPath}: no marketplace manifest there (expected .claude-plugin/marketplace.json under ${marketplaceDir})`);
1966
+ throw err;
1967
+ }
1968
+ let parsed;
1969
+ try {
1970
+ parsed = JSON.parse(raw);
1971
+ } catch (err) {
1972
+ throw new PluginManagerError(`${manifestPath}: malformed JSON (${err.message})`);
1973
+ }
1974
+ if (typeof parsed !== "object" || parsed === null)
1975
+ throw new PluginManagerError(`${manifestPath}: expected a JSON object at the top level`);
1976
+ const obj = parsed;
1977
+ if (typeof obj.name !== "string" || obj.name.length === 0)
1978
+ throw new PluginManagerError(`${manifestPath}: missing required "name"`);
1979
+ if (!Array.isArray(obj.plugins))
1980
+ throw new PluginManagerError(`${manifestPath}: missing required "plugins" array`);
1981
+ return {
1982
+ name: obj.name,
1983
+ plugins: obj.plugins,
1984
+ ...obj.version !== undefined ? { version: obj.version } : {},
1985
+ ...obj.description !== undefined ? { description: obj.description } : {},
1986
+ ...obj.metadata !== undefined ? { metadata: obj.metadata } : {}
1987
+ };
1988
+ }
1989
+ function classifySource(source) {
1990
+ if (/^https?:\/\//i.test(source))
1991
+ return { kind: "url", locator: source };
1992
+ if (/^git@[^:]+:/i.test(source) || /^git\+/i.test(source) || /\.git$/i.test(source))
1993
+ return { kind: "git", locator: source };
1994
+ if (/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?\/[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(source) && !source.includes("..")) {
1995
+ return { kind: "github", locator: source };
1996
+ }
1997
+ return { kind: "directory", locator: isAbsolute2(source) ? source : resolve2(source) };
1998
+ }
1999
+ function marketplaceInfoOf(name, record) {
2000
+ const kind = record.source.source;
2001
+ const source = record.source.path ?? record.source.url ?? record.source.repo ?? record.installLocation;
2002
+ return { name, source, kind, path: record.installLocation };
2003
+ }
2004
+ function parseSpec(spec) {
2005
+ const at = spec.lastIndexOf("@");
2006
+ if (at <= 0 || at === spec.length - 1) {
2007
+ throw new PluginManagerError(`plugin spec must be "<name>@<marketplace>" (got ${JSON.stringify(spec)}) -- bare-name marketplace inference is not implemented in this build`);
2008
+ }
2009
+ return { name: spec.slice(0, at), marketplace: spec.slice(at + 1) };
2010
+ }
2011
+ function keyFor(name, marketplace) {
2012
+ return `${name}@${marketplace}`;
2013
+ }
2014
+ async function readEnabledFromSettings(o, scope, key) {
2015
+ const loaded = await loadSettingsFile(o.settingsPathFor(scope));
2016
+ return loaded.values.enabledPlugins?.[key] === true;
2017
+ }
2018
+ function assertSettingsWritable(loaded, path) {
2019
+ if (loaded.present && !loaded.loaded) {
2020
+ throw new PluginManagerError(`${path}: cannot update enabledPlugins -- the file exists but ${loaded.error ?? "could not be read"}; fix it by hand first`);
2021
+ }
2022
+ }
2023
+ async function setEnabledInSettings(o, scope, key, enabled) {
2024
+ const path = o.settingsPathFor(scope);
2025
+ const loaded = await loadSettingsFile(path);
2026
+ assertSettingsWritable(loaded, path);
2027
+ const settings = { ...loaded.values };
2028
+ const enabledPlugins = { ...settings.enabledPlugins ?? {} };
2029
+ if (enabled === undefined)
2030
+ delete enabledPlugins[key];
2031
+ else
2032
+ enabledPlugins[key] = enabled;
2033
+ settings.enabledPlugins = enabledPlugins;
2034
+ await mkdir(dirname3(path), { recursive: true });
2035
+ await writeJsonAtomicNoLock(path, settings);
2036
+ }
2037
+ async function listMarketplaces(o) {
2038
+ const file = await readKnownMarketplacesFile(o);
2039
+ return Object.entries(file).map(([name, rec]) => marketplaceInfoOf(name, rec));
2040
+ }
2041
+ async function addMarketplace(o, source) {
2042
+ const classification = classifySource(source);
2043
+ if (classification.kind !== "directory") {
2044
+ throw new PluginManagerError(`marketplace source kind "${classification.kind}" is not supported in this build (no network) -- only local directory marketplaces can be added; got ${JSON.stringify(source)}`);
2045
+ }
2046
+ const dir = classification.locator;
2047
+ const manifest = await readDirectoryMarketplaceManifest(dir);
2048
+ await mkdir(o.pluginsRoot, { recursive: true });
2049
+ const path = knownMarketplacesPath(o);
2050
+ const record = {
2051
+ source: { source: "directory", path: dir },
2052
+ installLocation: dir,
2053
+ lastUpdated: new Date().toISOString(),
2054
+ autoUpdate: false
2055
+ };
2056
+ await withMarketplacesLock(path, async () => {
2057
+ const file = await readKnownMarketplacesFile(o);
2058
+ file[manifest.name] = record;
2059
+ await writeKnownMarketplacesFile(path, file);
2060
+ });
2061
+ return marketplaceInfoOf(manifest.name, record);
2062
+ }
2063
+ async function removeMarketplace(o, name) {
2064
+ const path = knownMarketplacesPath(o);
2065
+ await withMarketplacesLock(path, async () => {
2066
+ const file = await readKnownMarketplacesFile(o);
2067
+ if (!(name in file))
2068
+ throw new PluginManagerError(`marketplace "${name}" is not known`);
2069
+ delete file[name];
2070
+ await writeKnownMarketplacesFile(path, file);
2071
+ });
2072
+ }
2073
+ async function updateMarketplace(o, name) {
2074
+ const path = knownMarketplacesPath(o);
2075
+ await withMarketplacesLock(path, async () => {
2076
+ const file = await readKnownMarketplacesFile(o);
2077
+ const names = name !== undefined ? [name] : Object.keys(file);
2078
+ for (const n of names) {
2079
+ const rec = file[n];
2080
+ if (rec === undefined)
2081
+ throw new PluginManagerError(`marketplace "${n}" is not known`);
2082
+ if (rec.source.source !== "directory") {
2083
+ throw new PluginManagerError(`marketplace "${n}" has source kind "${rec.source.source}", which this build cannot refresh (no network)`);
2084
+ }
2085
+ await readDirectoryMarketplaceManifest(rec.installLocation);
2086
+ rec.lastUpdated = new Date().toISOString();
2087
+ }
2088
+ await writeKnownMarketplacesFile(path, file);
2089
+ });
2090
+ }
2091
+ async function resolvePluginSourcePath(o, name, marketplace) {
2092
+ const marketplaces = await readKnownMarketplacesFile(o);
2093
+ const rec = marketplaces[marketplace];
2094
+ if (rec === undefined)
2095
+ throw new PluginManagerError(`marketplace "${marketplace}" is not known -- add it first`);
2096
+ if (rec.source.source !== "directory") {
2097
+ throw new PluginManagerError(`marketplace "${marketplace}" has source kind "${rec.source.source}", which this build cannot install from (no network)`);
2098
+ }
2099
+ const manifest = await readDirectoryMarketplaceManifest(rec.installLocation);
2100
+ const entry = manifest.plugins.find((p) => p.name === name);
2101
+ if (entry === undefined)
2102
+ throw new PluginManagerError(`plugin "${name}" is not listed by marketplace "${marketplace}"`);
2103
+ if (typeof entry.source !== "string") {
2104
+ throw new PluginManagerError(`plugin "${name}@${marketplace}" declares a non-local source, which this build cannot install (no network)`);
2105
+ }
2106
+ const installPath = resolveMarketplacePluginPath(rec.installLocation, manifest.metadata?.pluginRoot, entry.source);
2107
+ if (installPath === undefined) {
2108
+ throw new PluginManagerError(`plugin "${name}@${marketplace}" declares a source (${JSON.stringify(entry.source)}) this build refuses to resolve -- it must be a "./relative/path" under the marketplace root (or a bare name when the marketplace's own metadata.pluginRoot is set), and must not escape the marketplace directory`);
2109
+ }
2110
+ return { installPath, version: entry.version };
2111
+ }
2112
+ async function installPlugin(o, spec, scope) {
2113
+ const { name, marketplace } = parseSpec(spec);
2114
+ const settingsPath = o.settingsPathFor(scope);
2115
+ assertSettingsWritable(await loadSettingsFile(settingsPath), settingsPath);
2116
+ const { installPath, version } = await resolvePluginSourcePath(o, name, marketplace);
2117
+ const key = keyFor(name, marketplace);
2118
+ await mkdir(o.pluginsRoot, { recursive: true });
2119
+ const now = new Date().toISOString();
2120
+ const record = { scope, installPath, installedAt: now, ...version !== undefined ? { version } : {} };
2121
+ const file = await readInstalledPluginsFile(o);
2122
+ const existing = file.plugins[key] ?? [];
2123
+ file.plugins[key] = [...existing.filter((r) => r.scope !== scope), record];
2124
+ await writeJsonAtomicNoLock(installedPluginsPath(o), file);
2125
+ await setEnabledInSettings(o, scope, key, true);
2126
+ return { id: name, installPath, scope, ...version !== undefined ? { version } : {} };
2127
+ }
2128
+ async function uninstallPlugin(o, spec, scope) {
2129
+ const { name, marketplace } = parseSpec(spec);
2130
+ const key = keyFor(name, marketplace);
2131
+ const settingsPath = o.settingsPathFor(scope);
2132
+ assertSettingsWritable(await loadSettingsFile(settingsPath), settingsPath);
2133
+ const file = await readInstalledPluginsFile(o);
2134
+ const existing = file.plugins[key];
2135
+ const remaining = (existing ?? []).filter((r) => r.scope !== scope);
2136
+ if (existing === undefined || remaining.length === existing.length) {
2137
+ throw new PluginManagerError(`plugin "${key}" is not installed at scope "${scope}"`);
2138
+ }
2139
+ if (remaining.length > 0)
2140
+ file.plugins[key] = remaining;
2141
+ else
2142
+ delete file.plugins[key];
2143
+ await writeJsonAtomicNoLock(installedPluginsPath(o), file);
2144
+ await setEnabledInSettings(o, scope, key, undefined);
2145
+ }
2146
+ async function setPluginEnabled(o, spec, scope, enabled) {
2147
+ const { name, marketplace } = parseSpec(spec);
2148
+ const key = keyFor(name, marketplace);
2149
+ const file = await readInstalledPluginsFile(o);
2150
+ if (!(file.plugins[key]?.some((r) => r.scope === scope) ?? false)) {
2151
+ throw new PluginManagerError(`plugin "${key}" is not installed at scope "${scope}"`);
2152
+ }
2153
+ await setEnabledInSettings(o, scope, key, enabled);
2154
+ }
2155
+ async function updatePlugin(o, spec) {
2156
+ const { name, marketplace } = parseSpec(spec);
2157
+ const key = keyFor(name, marketplace);
2158
+ const file = await readInstalledPluginsFile(o);
2159
+ const records = file.plugins[key];
2160
+ if (records === undefined || records.length === 0)
2161
+ throw new PluginManagerError(`plugin "${key}" is not installed`);
2162
+ const { installPath, version } = await resolvePluginSourcePath(o, name, marketplace);
2163
+ const now = new Date().toISOString();
2164
+ const updated = records.map((r) => ({ ...r, installPath, lastUpdated: now, ...version !== undefined ? { version } : {} }));
2165
+ file.plugins[key] = updated;
2166
+ await writeJsonAtomicNoLock(installedPluginsPath(o), file);
2167
+ const last = updated[updated.length - 1];
2168
+ return { id: name, installPath, scope: last.scope, ...version !== undefined ? { version } : {} };
2169
+ }
2170
+ async function listPlugins(o) {
2171
+ const file = await readInstalledPluginsFile(o);
2172
+ const out = [];
2173
+ for (const [key, records] of Object.entries(file.plugins)) {
2174
+ const at = key.lastIndexOf("@");
2175
+ const name = at > 0 ? key.slice(0, at) : key;
2176
+ const marketplace = at > 0 ? key.slice(at + 1) : "";
2177
+ for (const rec of records) {
2178
+ const enabled = await readEnabledFromSettings(o, rec.scope, key);
2179
+ out.push({ id: name, installPath: rec.installPath, scope: rec.scope, enabled, marketplace, ...rec.version !== undefined ? { version: rec.version } : {} });
2180
+ }
2181
+ }
2182
+ return out;
2183
+ }
1695
2184
  export {
1696
2185
  AbortError,
1697
2186
  BRAND_TOKEN_RE2 as BRAND_TOKEN_RE,
2187
+ CATALOG_TAG_RENAMES,
1698
2188
  CLIConnectionError,
1699
2189
  DEFAULT_COMPACTION_THRESHOLD,
1700
2190
  DEFAULT_CONTEXT_WINDOW_TOKENS,
@@ -1715,6 +2205,7 @@ export {
1715
2205
  PROJECT_PERMISSIVE_KEYS,
1716
2206
  PROTOCOL_VERSION,
1717
2207
  PROVIDER_STATE_FILE_SUFFIX2 as PROVIDER_STATE_FILE_SUFFIX,
2208
+ PluginManagerError,
1718
2209
  ProcessError,
1719
2210
  ProtocolDecodeError,
1720
2211
  ProtocolError,
@@ -1732,11 +2223,13 @@ export {
1732
2223
  WinterSDKError,
1733
2224
  WinterStoreError2 as WinterStoreError,
1734
2225
  WinterStoreLeaseError2 as WinterStoreLeaseError,
2226
+ addMarketplace,
1735
2227
  applyWorkspaceTrust,
1736
2228
  compatibilityKeys,
1737
2229
  decodeFrame,
1738
2230
  defaultSpawn,
1739
2231
  deleteSession,
2232
+ disableCronEnvName,
1740
2233
  encodeFrame,
1741
2234
  envName2 as envName,
1742
2235
  filterEscalatingDefaultMode,
@@ -1745,6 +2238,8 @@ export {
1745
2238
  getSessionInfo,
1746
2239
  getSessionMessages,
1747
2240
  getSubagentMessages,
2241
+ globalConfigFileName,
2242
+ installPlugin,
1748
2243
  isDeliveryOutcome,
1749
2244
  isGlobalAgentMessage,
1750
2245
  isListedRuntimeObjectArray,
@@ -1760,25 +2255,36 @@ export {
1760
2255
  isUnset2 as isUnset,
1761
2256
  isVendorCompliantProjectKey,
1762
2257
  isWinterMcpServerInstance,
2258
+ listMarketplaces,
2259
+ listPlugins,
1763
2260
  listSessions,
1764
2261
  listSubagents,
1765
2262
  loadSettingsFile,
1766
2263
  mcpToolName2 as mcpToolName,
2264
+ pluginCacheDirEnvName,
2265
+ providerManagedByHostEnvName,
1767
2266
  providerSettingsFrom,
1768
2267
  query,
2268
+ removeMarketplace,
1769
2269
  renameSession,
1770
2270
  resolveBrand2 as resolveBrand,
1771
2271
  resolveFacetTarget,
1772
2272
  resolveKeychainServiceForProfile2 as resolveKeychainServiceForProfile,
2273
+ resolveMarketplacePluginPath,
1773
2274
  resolveRuntimeExecutable,
1774
2275
  resolveSettings,
1775
2276
  resolveSettingsDetailed,
1776
2277
  resolveWebToolsConfig,
1777
2278
  resolveWinterHome2 as resolveWinterHome,
2279
+ setPluginEnabled,
1778
2280
  settingsPathFor,
1779
2281
  splitFrames,
2282
+ storeHomeEnvName,
1780
2283
  tagSession,
1781
2284
  transcriptProjectKey,
2285
+ uninstallPlugin,
2286
+ updateMarketplace,
2287
+ updatePlugin,
1782
2288
  userAgent2 as userAgent,
1783
2289
  validateModelSlots
1784
2290
  };
package/dist/options.d.ts CHANGED
@@ -275,7 +275,9 @@ export interface Options {
275
275
  autoClassifier?: AutoClassifierConfig;
276
276
  /**
277
277
  * DISCLOSED WINTER option (P2 carry, wired in T10): the advisor/reviewer backend's model, same
278
- * selection path. Its optional `authRef` (fix wave, Ruling E-1) is the advisor's OWN credential:
278
+ * selection path -- a provider-qualified tag (`<providerId>/<model>`) names its own provider, so a
279
+ * reviewer on another provider than the session's is just that tag plus that provider's credential
280
+ * (the `web.fetch.digestModel` shape). Its optional `authRef` (fix wave, Ruling E-1) is the advisor's OWN credential:
279
281
  * a target on another provider than the session's never inherits the session's -- it uses the
280
282
  * route's ref, else the target provider's own keychain record (`<providerId>:default`), else a
281
283
  * typed `no-credential-for-provider` refusal at its first generation.
@@ -0,0 +1,7 @@
1
+ import type { BrandProfile } from "../brand.js";
2
+ /**
3
+ * `<brand.homeDirName>.json` -- `.winter.json` for Winter's own brand. `brand.homeDirName` is
4
+ * documented (`brand.ts`) as never containing a slash, so this is always a single path segment,
5
+ * safe to `join` onto any directory without further validation.
6
+ */
7
+ export declare function globalConfigFileName(brand: Pick<BrandProfile, "homeDirName">): string;
@@ -19,9 +19,21 @@ export type HomeBrand = Pick<BrandProfile, "envPrefix" | "homeDirName">;
19
19
  * (packages/runtime/src/brand-gate.test.ts, rules 9 and 10) is what keeps it that way.
20
20
  *
21
21
  * PRECEDENCE (WS-01 §2.2, and the same rule the current daemon has always had): an explicit
22
- * `<PREFIX>HOME` wins over EVERYTHING, including the dev profile. `<PREFIX>PROFILE=dev` selects
23
- * `~/<homeDirName>-dev` — the dev/dist split, so a development build can never share a home (or a
24
- * transcript store, or a settings file) with the copy a user actually runs.
22
+ * `<PREFIX>HOME` wins over EVERYTHING, including the dev profile and the WS-21 `sdk` suffix below.
23
+ * `<PREFIX>PROFILE=dev` selects `~/<homeDirName>-dev/sdk` — the dev/dist split, so a development
24
+ * build can never share a home (or a transcript store, or a settings file) with the copy a user
25
+ * actually runs.
26
+ *
27
+ * WS-21 §6.3 item 8: the DEFAULT (no `<PREFIX>HOME`, no explicit `brand`-threaded value) is now
28
+ * `~/<homeDirName>/sdk`, not `~/<homeDirName>` -- Contract A's `sdkHomeOf(home) = join(home, "sdk")`,
29
+ * inlined here rather than imported (the router that exports it is a separate package this SDK
30
+ * never depends on). `brand.homeDirName` cannot contain a slash (`brand.ts`'s own validation), so
31
+ * this is always exactly one extra path segment. This changes ONLY the bare-default case: every
32
+ * router-routed session gets an EXPLICIT `WINTER_HOME` (the Global Constraints list: "only the
33
+ * router sets ... WINTER_HOME"), which still wins outright, and the daemon's own pre-router-link
34
+ * fallback threads its own value too (`session-driver.ts`'s `WINTER_HOME` default, unaffected by
35
+ * this function). A bare `winter` invocation with no env override is the only caller this moves --
36
+ * consistent with the shared runtime home being the thing the SDK itself now means by "home".
25
37
  */
26
38
  export declare function resolveWinterHome(env?: Record<string, string | undefined>, brand?: HomeBrand): string;
27
39
  /**
@@ -0,0 +1,11 @@
1
+ import { type BrandProfile } from "../brand.js";
2
+ type EnvPrefixBrand = Pick<BrandProfile, "envPrefix">;
3
+ /** `WINTER_STORE_HOME` -- the shared runtime home's durable-paths root (spec §3.7). No claude twin. */
4
+ export declare function storeHomeEnvName(brand: EnvPrefixBrand): string;
5
+ /** `WINTER_PLUGIN_CACHE_DIR` -- the twin of `CLAUDE_CODE_PLUGIN_CACHE_DIR` (F15). */
6
+ export declare function pluginCacheDirEnvName(brand: EnvPrefixBrand): string;
7
+ /** `WINTER_PROVIDER_MANAGED_BY_HOST` -- the twin of `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` (F20). */
8
+ export declare function providerManagedByHostEnvName(brand: EnvPrefixBrand): string;
9
+ /** `WINTER_DISABLE_CRON` -- the twin of `CLAUDE_CODE_DISABLE_CRON` (F19a). */
10
+ export declare function disableCronEnvName(brand: EnvPrefixBrand): string;
11
+ export {};
@@ -0,0 +1,37 @@
1
+ export declare class PluginManagerError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export type PluginScope = "user" | "project" | "local";
5
+ export interface PluginManagerOptions {
6
+ /** claude's plugins root (WS-21 §6.3 item 5: `storeHome/plugins`) -- `installed_plugins.json` and `known_marketplaces.json` live directly under it. */
7
+ pluginsRoot: string;
8
+ /** The `enabledPlugins` carrier for one scope -- the settings file at that tier (the shared runtime home's own `settings.json` for `user`; the project dir's `settings.json`/`settings.local.json` for `project`/`local`). */
9
+ settingsPathFor(scope: PluginScope): string;
10
+ }
11
+ export interface MarketplaceInfo {
12
+ name: string;
13
+ /** The human-facing locator: the directory path, the repo `owner/repo`, or the URL -- whichever the source kind carries. */
14
+ source: string;
15
+ kind: "directory" | "git" | "github" | "url";
16
+ /** Where the marketplace's own content lives on disk. For `directory`, this IS the source (read in place, F15). */
17
+ path: string;
18
+ }
19
+ export interface InstalledPlugin {
20
+ id: string;
21
+ version?: string;
22
+ installPath: string;
23
+ scope: PluginScope;
24
+ }
25
+ export interface PluginListing extends InstalledPlugin {
26
+ enabled: boolean;
27
+ marketplace: string;
28
+ }
29
+ export declare function listMarketplaces(o: PluginManagerOptions): Promise<MarketplaceInfo[]>;
30
+ export declare function addMarketplace(o: PluginManagerOptions, source: string): Promise<MarketplaceInfo>;
31
+ export declare function removeMarketplace(o: PluginManagerOptions, name: string): Promise<void>;
32
+ export declare function updateMarketplace(o: PluginManagerOptions, name?: string): Promise<void>;
33
+ export declare function installPlugin(o: PluginManagerOptions, spec: string, scope: PluginScope): Promise<InstalledPlugin>;
34
+ export declare function uninstallPlugin(o: PluginManagerOptions, spec: string, scope: PluginScope): Promise<void>;
35
+ export declare function setPluginEnabled(o: PluginManagerOptions, spec: string, scope: PluginScope, enabled: boolean): Promise<void>;
36
+ export declare function updatePlugin(o: PluginManagerOptions, spec: string): Promise<InstalledPlugin>;
37
+ export declare function listPlugins(o: PluginManagerOptions): Promise<PluginListing[]>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve one marketplace manifest plugin entry's install path -- the ONE function
3
+ * `packages/runtime/src/plugins/installed.ts` (the daemon's own directory-marketplace fallback,
4
+ * SV-4) and `packages/sdk/src/plugins/manage.ts` (`resolvePluginSourcePath`, the CLI-facing install
5
+ * command) both call, so the two can never resolve the identical manifest entry to two different
6
+ * paths. `undefined` for anything invalid or refused -- a bare name with no usable `pluginRoot`, an
7
+ * absolute/malformed source, or a path that would escape `installLocation` -- NEVER a throw a
8
+ * caller must remember to catch; each caller renders its own typed rejection from `undefined`.
9
+ */
10
+ export declare function resolveMarketplacePluginPath(installLocation: string, pluginRoot: unknown, source: unknown): string | undefined;
@@ -88,7 +88,9 @@ export interface SandboxSettingsConfig {
88
88
  filesystem?: {
89
89
  allowWrite?: string[];
90
90
  denyWrite?: string[];
91
+ allowRead?: string[];
91
92
  denyRead?: string[];
93
+ allowGitConfig?: boolean;
92
94
  };
93
95
  network?: {
94
96
  allowedDomains?: string[];
@@ -219,6 +221,17 @@ export interface RuntimeConfig {
219
221
  resumeDropsTurn?: boolean;
220
222
  persistSession?: boolean;
221
223
  winterHome?: string;
224
+ /**
225
+ * WS-21 §3.7/§6.3 item 11: the shared runtime home's durable-paths root (`storeHomeEnvName`'s
226
+ * value) and the claude-`CLAUDE_CODE_PLUGIN_CACHE_DIR` twin, next to `winterHome` for a host that
227
+ * assembles `--config-json` directly rather than only setting the child's env. The CANONICAL
228
+ * channel is still the child's own env (`production-wiring.ts` reads `config.storeHome ??
229
+ * env[storeHomeEnvName(brand)]`, the same precedence `winterHome` already has over
230
+ * `resolveWinterHome(env, brand)`) -- these two fields exist so a host with no separate env-setting
231
+ * step has an equivalent one.
232
+ */
233
+ storeHome?: string;
234
+ pluginCacheDir?: string;
222
235
  allowedTools?: string[];
223
236
  disallowedTools?: string[];
224
237
  permissions?: {
@@ -473,6 +486,18 @@ export interface AutoClassifierConfig {
473
486
  * advisor's provider, the advisor's provider refuses with a typed `no-credential-for-provider`.
474
487
  */
475
488
  export interface AdvisorConfig {
489
+ /**
490
+ * The reviewer's model: a PROVIDER-QUALIFIED TAG (`<providerId>/<model>`, e.g.
491
+ * `codex-oauth/gpt-5.6-sol`) or a slot name, resolved through the SAME selection path as the
492
+ * session model -- exactly as `WebFetchConfig.digestModel` is.
493
+ *
494
+ * THE TAG IS THE PROVIDER IDENTITY; there is deliberately no second field for it. A qualified tag
495
+ * resolves under ITS OWN provider, never under the session's (`Options.provider`), so an advisor on
496
+ * another provider than the session's needs nothing but that tag and, in `authRef`, that provider's
497
+ * own credential. A host that splits the tag down to a bare model id before passing it has thrown
498
+ * the provider away: a bare id is read as a slot name or canonical id and resolved by the session's
499
+ * slot rules, which is a different question.
500
+ */
476
501
  model: string;
477
502
  authRef?: CredentialRef;
478
503
  }
@@ -42,11 +42,56 @@ export interface ControlResponseFrame {
42
42
  message: string;
43
43
  };
44
44
  }
45
+ /**
46
+ * Runtime -> host: "I stopped waiting for control_request `requestId`" (lane C, C2 -- claude parity:
47
+ * claude's structuredIO writes `control_cancel_request` when a pending request's signal aborts, e.g.
48
+ * a turn interrupted while a permission prompt is open). The host aborts the callback it is running
49
+ * for that request (`canUseTool`'s `signal`) and owes no response; one sent anyway is dropped.
50
+ */
51
+ /**
52
+ * `result.usage`: claude's `NonNullableUsage` (every `BetaUsage` field, none null), carrying THIS
53
+ * turn's main-loop generations -- claude builds a fresh QueryEngine per prompt in SDK mode, so its
54
+ * `totalUsage` starts at EMPTY_USAGE each turn. The four token counts are real; the rest is filled the
55
+ * way the pinned binary's own EMPTY_USAGE fills a value it has no data for, and says so:
56
+ * - `cache_creation`: every cache write Winter marks is the default 5-minute ephemeral one (the
57
+ * adapters send `{type: "ephemeral"}` with no ttl), so `ephemeral_5m_input_tokens` equals
58
+ * `cache_creation_input_tokens` and `ephemeral_1h_input_tokens` is 0;
59
+ * - `server_tool_use`: Winter's WebSearch/WebFetch run client-side, never as billed server tools: 0/0;
60
+ * - `output_tokens_details.thinking_tokens`: 0 -- providers' reasoning-token counts are not threaded
61
+ * through Winter's usage yet (they are included in `output_tokens`);
62
+ * - `service_tier: "standard"`, `inference_geo: ""`, `iterations: []`, `speed: "standard"`: the
63
+ * pinned EMPTY_USAGE's own values.
64
+ */
65
+ export interface WireResultUsage {
66
+ output_tokens_details: {
67
+ thinking_tokens: number;
68
+ };
69
+ input_tokens: number;
70
+ cache_creation_input_tokens: number;
71
+ cache_read_input_tokens: number;
72
+ output_tokens: number;
73
+ server_tool_use: {
74
+ web_search_requests: number;
75
+ web_fetch_requests: number;
76
+ };
77
+ service_tier: "standard" | "priority" | "batch";
78
+ cache_creation: {
79
+ ephemeral_1h_input_tokens: number;
80
+ ephemeral_5m_input_tokens: number;
81
+ };
82
+ inference_geo: string;
83
+ iterations: unknown[];
84
+ speed: "standard" | "fast";
85
+ }
86
+ export interface ControlCancelRequestFrame {
87
+ type: "control_cancel_request";
88
+ requestId: string;
89
+ }
45
90
  export interface UnknownFrame {
46
91
  type: string;
47
92
  [k: string]: unknown;
48
93
  }
49
- export type WinterFrame = InitFrame | UserFrame | DataFrame | ControlRequestFrame | ControlResponseFrame | UnknownFrame;
94
+ export type WinterFrame = InitFrame | UserFrame | DataFrame | ControlRequestFrame | ControlResponseFrame | ControlCancelRequestFrame | UnknownFrame;
50
95
  export interface SDKHookStartedMessage {
51
96
  type: "system";
52
97
  subtype: "hook_started";
@@ -618,6 +663,8 @@ export type SdkMessage = {
618
663
  structured_output?: unknown;
619
664
  terminal_reason?: "structured_output_retry_exhausted" | "api_error" | string;
620
665
  api_error_status?: number | null;
666
+ /** THIS turn's main-loop usage -- claude's `result.usage` (dist-session fixes C1); see `WireResultUsage`. */
667
+ usage?: WireResultUsage;
621
668
  permission_denials: SDKPermissionDenial[];
622
669
  [k: string]: unknown;
623
670
  } | {
package/dist/query.d.ts CHANGED
@@ -21,7 +21,14 @@ export type ControlRequestHandlerResult = {
21
21
  message: string;
22
22
  };
23
23
  };
24
- export type ControlRequestHandler = (payload: unknown) => Promise<ControlRequestHandlerResult>;
24
+ /**
25
+ * A handler for one runtime-originated control_request subtype. `ctx.signal` aborts when the runtime
26
+ * CANCELS the request (`control_cancel_request` -- lane C, C2, claude parity): the handler should stop
27
+ * waiting, and whatever it returns afterwards is not sent (the runtime already stopped waiting).
28
+ */
29
+ export type ControlRequestHandler = (payload: unknown, ctx?: {
30
+ signal: AbortSignal;
31
+ }) => Promise<ControlRequestHandlerResult>;
25
32
  export interface QueryInternal {
26
33
  registerControlRequestHandler(subtype: string, handler: ControlRequestHandler): void;
27
34
  respondPermission(requestId: string, result: PermissionResult): void;
@@ -5,7 +5,7 @@ import type { BrandProfile } from "../brand.js";
5
5
  * caller can thread three fields rather than construct one.
6
6
  */
7
7
  export type SettingsBrand = Pick<BrandProfile, "envPrefix" | "homeDirName" | "projectDirName">;
8
- import type { ModelSlotSetting } from "../protocol/config.js";
8
+ import type { ModelSlotSetting, SandboxSettingsConfig } from "../protocol/config.js";
9
9
  /** The three settings FILE tiers a session may load. `sdk.d.ts:7917`, verbatim and in pinned order. */
10
10
  export type SettingSource = "user" | "project" | "local";
11
11
  /** Pinned order (`sdk.d.ts:7917`). Also the value `settingSources: undefined` means (all three). */
@@ -39,6 +39,35 @@ export interface SettingsPermissionsBlock {
39
39
  }
40
40
  export interface Settings {
41
41
  permissions?: SettingsPermissionsBlock;
42
+ /**
43
+ * SV-11 (WS-21 fix round 9, security): the CC-shaped sandbox configuration surface, same shape as
44
+ * `RuntimeConfig.sandbox` (`protocol/config.ts`'s own `SandboxSettingsConfig`, kept structurally
45
+ * identical to `packages/runtime/src/sandbox/profile.ts`'s `SandboxSettings` for the same
46
+ * WS-02 §3 reason that type's own header states). Before this field existed, Winter's runtime read
47
+ * ONLY the host-supplied `RuntimeConfig.sandbox`, ignoring `settings.json`'s own `sandbox` block
48
+ * entirely -- a denied write from a plain, un-escalated `sandbox.filesystem.denyWrite` path went
49
+ * through unsandboxed, silently, because nothing ever threaded that key into the running sandbox at
50
+ * all (not a merge-precedence bug; there was no merge). `production-wiring.ts` now feeds the host's
51
+ * own `RuntimeConfig.sandbox` into this resolution as the `flag` tier (claude's own architecture:
52
+ * its SDK `Options` occupies the identical tier position for this same setting), and reads the
53
+ * result back off `effective.sandbox` into the config it actually hands the engine.
54
+ *
55
+ * ALL FOUR `filesystem` arrays -- `allowWrite`, `denyWrite`, `allowRead`, `denyRead` -- UNION
56
+ * across every tier (deep-merge's ordinary replace-by-higher-tier would let a higher tier
57
+ * silently drop a lower tier's own restriction -- the identical fail-open shape
58
+ * `permissions.deny`'s own union exists to prevent; see `unionSandboxFilesystemDenyArrays` below).
59
+ * CORRECTED in fix round 10, item C: round 9 reasoned `allowWrite` should NOT union (a widening
60
+ * key should never accumulate) and used the ordinary replace-by-higher-tier merge for it instead
61
+ * -- the controller's round-10 ruling reverses this, dump-confirmed against claude's own live
62
+ * sandbox-reconciliation code unioning its OWN rule-derived allowWrite set with the native
63
+ * sandbox's currently-configured one (`xr=se([...yr,...$r.filter(...)])`, not a replacement).
64
+ * Claude itself unions the widening array too; ported here to match. `allowRead` (also new this
65
+ * round) is claude's own read carve-out (`getFsReadConfig`'s `allowWithinDeny`) -- carried and
66
+ * unioned identically for merge-parity, though `packages/runtime/src/sandbox/profile.ts`'s own
67
+ * `buildSeatbeltProfile` does not yet enforce it (a disclosed, fails-CLOSED gap: see that file's
68
+ * own `SandboxFilesystemSettings.allowRead` doc comment).
69
+ */
70
+ sandbox?: SandboxSettingsConfig;
42
71
  hooks?: SettingsHooksConfig;
43
72
  env?: Record<string, string>;
44
73
  apiKeyHelper?: string;
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  resolveWinterHome2,
10
10
  WinterCompatibilitySessionStore2
11
- } from "../index-qst9rwbb.js";
11
+ } from "../index-0xks1t4r.js";
12
12
 
13
13
  // src/tools/schemas.ts
14
14
  var SEND_MESSAGE_TO_MAX = 300;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanlinglabs/winter-agent-sdk",
3
- "version": "0.0.17",
3
+ "version": "0.0.22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,15 +45,15 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@yanlinglabs/winter-provider-catalog": "0.0.17"
48
+ "@yanlinglabs/winter-provider-catalog": "0.0.22"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@yanlinglabs/winter-agent-sdk-darwin-arm64": "0.0.17"
51
+ "@yanlinglabs/winter-agent-sdk-darwin-arm64": "0.0.22"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.4.0",
55
- "winter-agent-runtime": "0.0.17",
56
- "@yanlinglabs/winter-conformance": "0.0.17"
55
+ "@yanlinglabs/winter-conformance": "0.0.22",
56
+ "winter-agent-runtime": "0.0.22"
57
57
  },
58
58
  "scripts": {}
59
59
  }