@coralai/sps-plugin-api 0.12.0 → 0.14.0

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.
package/dist/index.d.ts CHANGED
@@ -1,16 +1,3 @@
1
- /**
2
- * @coralai/sps-plugin-api —— **sps 宿主半区的插件契约**。
3
- *
4
- * 插件装它拿类型:`ctx['sps.tools']` 这些服务的方法签名。
5
- *
6
- * 🔴 **这个包是契约的真源,宿主去符合它** —— 不是反过来。
7
- * 反过来(从宿主导出类型)的话,包永远滞后于实现,
8
- * 而作者看到的类型和实际行为对不上时,他没法判断该信哪个。
9
- * sps-cli 侧有一道一致性判据钉着:seam 与这里的接口对不上,**它自己的 check 会红**。
10
- *
11
- * ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
12
- * `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
13
- */
14
1
  export interface SpsToolSession {
15
2
  cwd: string;
16
3
  project?: string;
@@ -364,6 +351,53 @@ export interface MemoryGcStats {
364
351
  }
365
352
  /** 迁移了多少条 —— 键由后端自定(本地实现给 global/agents/projects)。 */
366
353
  export type MemoryMigrateStats = Record<string, number>;
354
+ export interface SpsWorkspaceProject {
355
+ /** 相对工作区根,如 `t<租户>/<产品>/<项目>`。 */
356
+ rel: string;
357
+ /** 绝对路径。 */
358
+ abs: string;
359
+ /** 布局第二段 —— 资产声明按它取。 */
360
+ product: string;
361
+ }
362
+ export interface SpsAssetScope {
363
+ /** 相对项目根的路径前缀。 */
364
+ include: string[];
365
+ /**
366
+ * 有没有用兜底。
367
+ * ⚠️ **这一格必须能被报出来** —— "范围内什么都没有"和"根本没声明范围"
368
+ * 读数一样,处置完全不同。
369
+ */
370
+ usedDefault: boolean;
371
+ }
372
+ /**
373
+ * 工作区布局(**只读**)。给需要遍历用户产物的插件用(如同步到对象存储)。
374
+ *
375
+ * ⚠️ `discover()` 与 `sps.projects.list()` 是**两个不同的集合**:
376
+ * 前者按目录布局扫,后者读注册表。目录里有而没注册的项目只出现在前者。
377
+ */
378
+ export interface SpsWorkspace {
379
+ /** 工作区根(绝对路径)。 */
380
+ root(): string;
381
+ /** 按布局约定发现项目。 */
382
+ discover(): Promise<SpsWorkspaceProject[]>;
383
+ /** 某产品的用户资产范围。没有声明时回落窄兜底,并把 `usedDefault` 置真。 */
384
+ assetScope(product: string): SpsAssetScope;
385
+ }
386
+ /**
387
+ * `p` 是否在 `root` 之内(含 root 自身)。
388
+ *
389
+ * 🔴 **必须比到分隔符**:裸 `startsWith(root)` 会把 `<root>-old` 当成 root 内。
390
+ */
391
+ export declare function isUnderRoot(p: string, root: string): boolean;
392
+ /**
393
+ * 把路径解析到**真实位置**(软链解开)。
394
+ * 目标可能尚不存在(输出文件),故解析其**父目录**的 realpath 再拼回文件名。
395
+ *
396
+ * ⚠️ **异步**:同步 fs 会阻塞事件循环(含健康探针与 SSE 心跳)。
397
+ */
398
+ export declare function realResolve(p: string): Promise<string>;
399
+ /** realpath 之后再判围栏。**这是唯一该被安全路径调用的那个**;字符串比较不算围栏。 */
400
+ export declare function isRealUnderRoot(p: string, root: string): Promise<boolean>;
367
401
  /** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
368
402
  export declare const MEMORY_REDACTED_CODE = "memory-redacted";
369
403
  /** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
@@ -391,21 +425,18 @@ export interface SpsStorageObject {
391
425
  size: number;
392
426
  }
393
427
  /**
394
- * 对象存储(读 / 列 / 写)。桶是共享面 —— 多节点下 studio 和工作区不在同一台机器上。
428
+ * 对象存储(读 / 列 / 写)。**由插件提供** —— 没装桶插件时这个服务不存在,
429
+ * inject 它的插件会 PENDING。
395
430
  *
396
- * 🔴 **它只搬字节,不做账。** id / 索引 / 缩略图 / 分类 / 配额 / 计费都在平台侧。
397
- *
398
- * ⚠️ **权限按前缀分,而且很紧**(单写方规则)。
399
- * 读不了的前缀会**抛**,不会回空 —— "库是空的"和"我没权限"混成一个,
400
- * 人会去补素材而不是去补策略。
401
431
  * ⚠️ 桶没配时三个方法都抛 ⇒ **先用 `configured()` 问一句**,别拿异常当分支。
432
+ * 🔴 读不了的前缀会**抛**,不会回空 —— "库是空的"和"我没权限"混成一个,
433
+ * 人会去补素材而不是去补策略。
402
434
  */
403
435
  export interface SpsStorage {
404
436
  configured(): boolean;
405
437
  /** 不存在 ⇒ `null`;连不上/没权限 ⇒ **抛**。 */
406
438
  get(key: string): Promise<Buffer | null>;
407
439
  list(prefix: string): Promise<SpsStorageObject[]>;
408
- /** ⚠️ `contentType` 自己决定 —— 桶是直连的,缺省会让预览变成一次下载。 */
409
440
  put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
410
441
  }
411
442
  /** 与 `SpsAsyncPoll` 同形 —— async 能力的 `poll` 可以**直接透传**。 */
@@ -622,6 +653,16 @@ export interface SpsAttachments {
622
653
  }>;
623
654
  }
624
655
  /** 插件 `inject` 里写的名字。 */
656
+ /**
657
+ * **由插件提供**的服务 —— 宿主装配后它们**不一定在**。
658
+ *
659
+ * 🔴 inject 它们的插件要接受"没装那个插件就 PENDING"这件事。
660
+ * ⚠️ 这张表是给门禁用的单一真源:宿主的 conformance 判据据此
661
+ * 豁免这些名字("契约里列的服务装配后都在"对它们不成立)。
662
+ * 2026-08-28 加,起因是把 `sps.storage` 从宿主挪进插件后,
663
+ * 那条判据把"实现搬家了"报成了"契约说谎"。
664
+ */
665
+ export declare const PLUGIN_PROVIDED_SERVICES: readonly ["sps.storage", "sps.memory"];
625
666
  export declare const SPS_SERVICES: {
626
667
  readonly tools: "sps.tools";
627
668
  readonly credentials: "sps.credentials";
@@ -633,7 +674,6 @@ export declare const SPS_SERVICES: {
633
674
  readonly projects: "sps.projects";
634
675
  readonly cards: "sps.cards";
635
676
  readonly jobs: "sps.jobs";
636
- readonly storage: "sps.storage";
637
677
  readonly subprocess: "sps.subprocess";
638
678
  readonly attachments: "sps.attachments";
639
679
  readonly media: "sps.media";
@@ -642,6 +682,8 @@ export declare const SPS_SERVICES: {
642
682
  readonly agents: "sps.agents";
643
683
  readonly repo: "sps.repo";
644
684
  readonly memory: "sps.memory";
685
+ readonly storage: "sps.storage";
686
+ readonly workspace: "sps.workspace";
645
687
  readonly notifier: "sps.notifier";
646
688
  };
647
689
  /** 插件 ctx 上能拿到的服务(只列已声明契约的那几个)。 */
@@ -656,13 +698,14 @@ export interface SpsPluginContext {
656
698
  'sps.projects': SpsProjects;
657
699
  'sps.cards': SpsCards;
658
700
  'sps.jobs': SpsJobs;
659
- 'sps.storage': SpsStorage;
660
701
  'sps.capabilities': SpsCapabilities;
661
702
  'sps.media': SpsMedia;
662
703
  'sps.im': SpsRegistryByKey;
663
704
  'sps.agents': SpsRegistryByFactory;
664
705
  'sps.repo': SpsRegistryByFactory;
665
706
  'sps.memory': SpsMemory;
707
+ 'sps.storage': SpsStorage;
708
+ 'sps.workspace': SpsWorkspace;
666
709
  'sps.notifier': SpsNotifier;
667
710
  'sps.subprocess': SpsSubprocess;
668
711
  'sps.attachments': SpsAttachments;
package/dist/index.js CHANGED
@@ -11,6 +11,55 @@
11
11
  * ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
12
12
  * `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
13
13
  */
14
+ import { realpath } from 'node:fs/promises';
15
+ import { dirname, join, resolve as nodeResolve, sep } from 'node:path';
16
+ // ── 路径围栏:所有插件共用的安全下限 ──────────────────────────────────────
17
+ //
18
+ // 🔴 **安全判据不能有第二份。** 本仓已经为这个形状付过三次代价:
19
+ // `providers/art/withinDir.ts`(字符串,理智检查)与 `console/lib/artPaths.ts`
20
+ // (realpath,安全边界)长得几乎一样、靠头注区分;`scan.ts` 用 Dirent 做对了、
21
+ // 隔壁用 `stat`(跟随软链)就漏了。
22
+ // > **一条安全规则做对过一次,不代表它在第二条路上也在** ——
23
+ // > 而两条路写法不同时,正是它只做一半的时候。
24
+ // ⇒ 插件要围栏时**从这里取**,别自己写第四份。
25
+ //
26
+ // ⚠️ 本节**只做围栏**,不回答"这个文件该不该同步/发布/服务" ——
27
+ // 那三者判据不同、各自命名。合并会制造"它们本该一致"的错觉,
28
+ // 而那个错觉比漂移本身更危险:下一个人会去"修复不一致",把三条用途不同的规则强行统一。
29
+ /**
30
+ * `p` 是否在 `root` 之内(含 root 自身)。
31
+ *
32
+ * 🔴 **必须比到分隔符**:裸 `startsWith(root)` 会把 `<root>-old` 当成 root 内。
33
+ */
34
+ export function isUnderRoot(p, root) {
35
+ const r = nodeResolve(root);
36
+ const t = nodeResolve(p);
37
+ return t === r || t.startsWith(r + sep);
38
+ }
39
+ /**
40
+ * 把路径解析到**真实位置**(软链解开)。
41
+ * 目标可能尚不存在(输出文件),故解析其**父目录**的 realpath 再拼回文件名。
42
+ *
43
+ * ⚠️ **异步**:同步 fs 会阻塞事件循环(含健康探针与 SSE 心跳)。
44
+ */
45
+ export async function realResolve(p) {
46
+ const abs = nodeResolve(p);
47
+ try {
48
+ return await realpath(abs);
49
+ }
50
+ catch {
51
+ try {
52
+ return join(await realpath(dirname(abs)), abs.slice(dirname(abs).length + 1));
53
+ }
54
+ catch {
55
+ return abs;
56
+ }
57
+ }
58
+ }
59
+ /** realpath 之后再判围栏。**这是唯一该被安全路径调用的那个**;字符串比较不算围栏。 */
60
+ export async function isRealUnderRoot(p, root) {
61
+ return isUnderRoot(await realResolve(p), await realResolve(root));
62
+ }
14
63
  // ── 后端可以往 worker 注入一个 Claude Code 插件 ─────────────────────────────
15
64
  //
16
65
  // 记忆的读路有两半:**pull**(agent 自己调 `memory_recall`)与 **push**(会话自动召回)。
@@ -68,6 +117,16 @@ export function buildMemoryDigest(entries, opts = {}) {
68
117
  }
69
118
  // ── 服务名 ───────────────────────────────────────────────────────────────
70
119
  /** 插件 `inject` 里写的名字。 */
120
+ /**
121
+ * **由插件提供**的服务 —— 宿主装配后它们**不一定在**。
122
+ *
123
+ * 🔴 inject 它们的插件要接受"没装那个插件就 PENDING"这件事。
124
+ * ⚠️ 这张表是给门禁用的单一真源:宿主的 conformance 判据据此
125
+ * 豁免这些名字("契约里列的服务装配后都在"对它们不成立)。
126
+ * 2026-08-28 加,起因是把 `sps.storage` 从宿主挪进插件后,
127
+ * 那条判据把"实现搬家了"报成了"契约说谎"。
128
+ */
129
+ export const PLUGIN_PROVIDED_SERVICES = ['sps.storage', 'sps.memory'];
71
130
  export const SPS_SERVICES = {
72
131
  tools: 'sps.tools',
73
132
  credentials: 'sps.credentials',
@@ -79,7 +138,6 @@ export const SPS_SERVICES = {
79
138
  projects: 'sps.projects',
80
139
  cards: 'sps.cards',
81
140
  jobs: 'sps.jobs',
82
- storage: 'sps.storage',
83
141
  subprocess: 'sps.subprocess',
84
142
  attachments: 'sps.attachments',
85
143
  media: 'sps.media',
@@ -88,5 +146,7 @@ export const SPS_SERVICES = {
88
146
  agents: 'sps.agents',
89
147
  repo: 'sps.repo',
90
148
  memory: 'sps.memory',
149
+ storage: 'sps.storage',
150
+ workspace: 'sps.workspace',
91
151
  notifier: 'sps.notifier',
92
152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralai/sps-plugin-api",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "sps 宿主半区的插件契约:ctx 上那些服务的方法签名。插件装它拿类型。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",