@capybara-hq/plugin-sdk 0.1.0-rc.5 → 0.1.0-rc.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.
- package/agent-docs/connector-templates.md +1 -0
- package/agent-docs/plugin-format.md +11 -0
- package/dist/cards.d.ts +7 -0
- package/dist/cards.js +4 -4
- package/dist/channel.d.ts +20 -1
- package/dist/connector-auth.d.ts +2 -0
- package/dist/connector-auth.js +15 -0
- package/dist/device-protocol.d.ts +118 -0
- package/dist/device-protocol.js +276 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/marketplace.d.ts +26 -0
- package/dist/marketplace.js +43 -1
- package/dist/plugin-manifest.d.ts +12 -0
- package/dist/plugin-manifest.js +61 -0
- package/dist/sandbox.d.ts +24 -2
- package/dist/sandbox.js +41 -8
- package/dist/setup-guide.d.ts +94 -0
- package/dist/setup-guide.js +135 -0
- package/dist/skill-invocation.d.ts +4 -3
- package/dist/skill-invocation.js +5 -4
- package/dist/skill.d.ts +7 -4
- package/dist/skill.js +9 -6
- package/package.json +1 -1
- package/schemas/connector-template.v1.schema.json +3 -0
- package/schemas/marketplace.v1.schema.json +14 -0
- package/schemas/plugin.v1.schema.json +136 -0
|
@@ -37,6 +37,7 @@ file is strict: an unknown key is an error. Fields (agent templates are listed u
|
|
|
37
37
|
| `skills` | array of object | no |
|
|
38
38
|
| `connectorTemplates` | array of object | no |
|
|
39
39
|
| `agents` | array of object | no |
|
|
40
|
+
| `setupGuides` | array of object | no |
|
|
40
41
|
| `extension` | object | no |
|
|
41
42
|
<!-- /generated -->
|
|
42
43
|
|
|
@@ -51,6 +52,16 @@ Rules that the schema cannot show:
|
|
|
51
52
|
Omit them to scan the default folders. Names and ids are unique within the plugin.
|
|
52
53
|
- `dependencies`: plugin name plus semver range; no self-dependency.
|
|
53
54
|
- `agents`: at most 64 templates (see `agent-templates`).
|
|
55
|
+
- `setupGuides` (at most 8): setup material shown where a person configures what the plugin
|
|
56
|
+
brings. Each guide has an `id`, a `place` (`channel-connection`: 新建渠道 and the channel detail;
|
|
57
|
+
the plugin's extension must provide `channel@1`), a `title` and `steps` (`{ en, zh? }`, plain
|
|
58
|
+
text, a backtick pair shows as inline code) and `files` (`./` paths ending in `.tmpl`, `.yaml`,
|
|
59
|
+
`.yml`, `.json` or `.txt`, at most 64 KiB of text, with a `title` and a `language` of `yaml`,
|
|
60
|
+
`json` or `text`). A step or file may carry `mode` (`long_connection` or `webhook`) to apply to
|
|
61
|
+
that inbound mode only; ship one file per mode instead of conditionals. The kernel fills
|
|
62
|
+
`{{publicUrl}}`, `{{webhookUrl}}`, `{{signInCallbackUrl}}`, `{{connectionName}}`, `{{orgName}}`,
|
|
63
|
+
`{{pluginName}}`, `{{pluginVersion}}` and `{{mode}}` (never a secret); in YAML and JSON put a
|
|
64
|
+
placeholder inside double quotes. `capybara-plugin pack` refuses any other placeholder.
|
|
54
65
|
- `extension` is written by the packer; never write it by hand.
|
|
55
66
|
|
|
56
67
|
## Importing
|
package/dist/cards.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export type ChannelCard = {
|
|
|
10
10
|
toolName: string;
|
|
11
11
|
appName?: string;
|
|
12
12
|
agentName: string;
|
|
13
|
+
/**
|
|
14
|
+
* One line saying what the call does, as the web shows it: the tool's confirmation summary
|
|
15
|
+
* (e.g. a Platform operation's), else the tool's title and key argument; may be empty.
|
|
16
|
+
* Never the raw call input (it may hold a whole prompt, and a group sees the card); the
|
|
17
|
+
* full input is behind `webUrl`. Plain text written by the kernel from model-controlled
|
|
18
|
+
* values: render it as text, never as markup. The name is kept from when it held the input.
|
|
19
|
+
*/
|
|
13
20
|
inputPreview: string;
|
|
14
21
|
scopes: ("once" | "session" | "always")[];
|
|
15
22
|
expiresAt: string;
|
package/dist/cards.js
CHANGED
|
@@ -92,7 +92,7 @@ export function validateChannelCard(card) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
95
|
+
* 把模型可控的值(审批标题、调用摘要、提问标题、回答摘要)放进代码里,退回文本里它们不会变成链接、强调或 HTML:
|
|
96
96
|
* 单行用行内代码,多行用围栏代码块(以换行开头),分隔符都比值里最长的反引号串更长(CommonMark 6.1 / 4.5)。
|
|
97
97
|
* 选代码而不是给标点加反斜杠:代码是 CommonMark / GFM 里唯一保证内容按原样显示的写法(反斜杠挡不住 GFM 的裸网址
|
|
98
98
|
* 自动链接,各渠道转换器对反斜杠的处理也不一致)。
|
|
@@ -119,7 +119,7 @@ const TEXT = {
|
|
|
119
119
|
connect: (app) => `[Connect] ${app} needs to be connected.`,
|
|
120
120
|
tool: "Tool",
|
|
121
121
|
agent: "Agent",
|
|
122
|
-
|
|
122
|
+
summary: "Summary",
|
|
123
123
|
status: "Status",
|
|
124
124
|
reason: "Reason",
|
|
125
125
|
expires: "Expires",
|
|
@@ -149,7 +149,7 @@ const TEXT = {
|
|
|
149
149
|
connect: (app) => `【连接】需要连接 ${app}。`,
|
|
150
150
|
tool: "工具",
|
|
151
151
|
agent: "Agent",
|
|
152
|
-
|
|
152
|
+
summary: "摘要",
|
|
153
153
|
status: "状态",
|
|
154
154
|
reason: "原因",
|
|
155
155
|
expires: "过期时间",
|
|
@@ -187,7 +187,7 @@ export function renderCardAsText(card, locale) {
|
|
|
187
187
|
withCode(t.approval, asCode(card.title)),
|
|
188
188
|
line(t.tool, card.appName ? `${card.appName} / ${card.toolName}` : card.toolName),
|
|
189
189
|
line(t.agent, card.agentName),
|
|
190
|
-
line(t.
|
|
190
|
+
...(card.inputPreview !== "" ? [line(t.summary, asCode(card.inputPreview))] : []),
|
|
191
191
|
line(t.status, card.decidedBy ? `${state} (${card.decidedBy})` : state),
|
|
192
192
|
];
|
|
193
193
|
if (card.state === "denied" && card.denyReason)
|
package/dist/channel.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ export interface InboundSender {
|
|
|
21
21
|
tenantId: string;
|
|
22
22
|
displayName?: string;
|
|
23
23
|
avatarUrl?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The sender's time zone on the platform, an IANA name (`Asia/Shanghai`; Slack `users.info`
|
|
26
|
+
* `user.tz`), when the platform gives one. Include it on `message` events when you can get it
|
|
27
|
+
* within the acknowledgement deadline (cache it per user); leave it out otherwise. It wins over
|
|
28
|
+
* the person's account setting and the org default for the run's date and zone (run context
|
|
29
|
+
* spec §3.4); a name the kernel does not know is ignored.
|
|
30
|
+
*/
|
|
31
|
+
timeZone?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The sender's language on the platform: `zh` / `en` or a BCP 47 tag (`zh-CN`, `en-US`; Slack
|
|
34
|
+
* `users.info` with `include_locale=true`, `user.locale`), when the platform gives one. The
|
|
35
|
+
* kernel normalizes it (`zh` and `zh-*` are Chinese, any other tag English); it wins over the
|
|
36
|
+
* person's latest web UI language and the org default (run context spec §3.4).
|
|
37
|
+
*/
|
|
38
|
+
locale?: string;
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
26
41
|
* 渠道交给内核的入站事件。插件的约定:
|
|
@@ -76,7 +91,11 @@ export interface ChannelSendArgs {
|
|
|
76
91
|
*/
|
|
77
92
|
message: OutboundMessage;
|
|
78
93
|
signal: AbortSignal;
|
|
79
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* The recipient's language (the sender's platform language, else their latest web language,
|
|
96
|
+
* else the org's): the wording the extension renders itself (card buttons, fixed words in the
|
|
97
|
+
* text fallback) follows it.
|
|
98
|
+
*/
|
|
80
99
|
locale: Locale;
|
|
81
100
|
/**
|
|
82
101
|
* 幂等键(可选):不超过 40 个 `[A-Za-z0-9_-]` 字符,同一条逻辑消息的每次重试(崩溃后其他副本续投、
|
package/dist/connector-auth.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const ConnectorAuthSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
50
50
|
client_secret_basic: "client_secret_basic";
|
|
51
51
|
client_secret_post: "client_secret_post";
|
|
52
52
|
}>>;
|
|
53
|
+
tokenTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
53
54
|
}, z.core.$strict>, z.ZodObject<{
|
|
54
55
|
type: z.ZodLiteral<"oauth_client">;
|
|
55
56
|
ownership: z.ZodLiteral<"org">;
|
|
@@ -59,6 +60,7 @@ export declare const ConnectorAuthSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
59
60
|
client_secret_basic: "client_secret_basic";
|
|
60
61
|
client_secret_post: "client_secret_post";
|
|
61
62
|
}>>;
|
|
63
|
+
tokenTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
64
|
}, z.core.$strict>], "type">;
|
|
63
65
|
/** 认证预设({@link ConnectorAuthSchema} 的输出)。 */
|
|
64
66
|
export type ConnectorAuthPreset = z.infer<typeof ConnectorAuthSchema>;
|
package/dist/connector-auth.js
CHANGED
|
@@ -23,6 +23,19 @@ const Scopes = z
|
|
|
23
23
|
.max(200)
|
|
24
24
|
.regex(/^[\x21\x23-\x5B\x5D-\x7E]+$/, "Invalid scope"))
|
|
25
25
|
.max(50);
|
|
26
|
+
/**
|
|
27
|
+
* More `token_type` values the token endpoint may return for a bearer token (case-insensitive;
|
|
28
|
+
* `Bearer` and Slack's `user` are always accepted). The token is still sent as
|
|
29
|
+
* `Authorization: Bearer`. Never `DPoP`: such a token is bound to a key the kernel does not hold.
|
|
30
|
+
*/
|
|
31
|
+
const TokenTypes = z
|
|
32
|
+
.array(z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(32)
|
|
36
|
+
.regex(/^[A-Za-z][A-Za-z0-9_.-]*$/, "Invalid token type")
|
|
37
|
+
.refine((t) => t.toLowerCase() !== "dpop", "DPoP tokens are not bearer tokens"))
|
|
38
|
+
.max(5);
|
|
26
39
|
/** 认证方式:只有类型与非密钥字段;凭据值走内核的 `credential` / 凭据接口。 */
|
|
27
40
|
export const ConnectorAuthSchema = z.discriminatedUnion("type", [
|
|
28
41
|
z.object({ type: z.literal("none") }).strict(),
|
|
@@ -57,6 +70,7 @@ export const ConnectorAuthSchema = z.discriminatedUnion("type", [
|
|
|
57
70
|
scopes: Scopes.optional(),
|
|
58
71
|
client: z.enum(["dcr", "manual"]),
|
|
59
72
|
tokenAuthMethod: TokenAuthMethod.optional(),
|
|
73
|
+
tokenTypes: TokenTypes.optional(),
|
|
60
74
|
})
|
|
61
75
|
.strict()
|
|
62
76
|
.superRefine((a, ctx) => {
|
|
@@ -91,6 +105,7 @@ export const ConnectorAuthSchema = z.discriminatedUnion("type", [
|
|
|
91
105
|
tokenUrl: OAuthUrl,
|
|
92
106
|
scopes: Scopes.optional(),
|
|
93
107
|
tokenAuthMethod: TokenAuthMethod.optional(),
|
|
108
|
+
tokenTypes: TokenTypes.optional(),
|
|
94
109
|
})
|
|
95
110
|
.strict(),
|
|
96
111
|
]);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device protocol v1: the WebSocket frames between the kernel and a `capybara-device` program
|
|
3
|
+
* (a connected machine that serves as an environment). Pure types and hand-written guards.
|
|
4
|
+
*/
|
|
5
|
+
import { type ExecResult, type SandboxActor, type SandboxLimits, type SandboxRoots } from "./sandbox.js";
|
|
6
|
+
/** The protocol version carried in `X-Capybara-Device-Protocol` and the hello frame. */
|
|
7
|
+
export declare const DEVICE_PROTOCOL_VERSION = 1;
|
|
8
|
+
/** The operations the kernel asks a device to run. */
|
|
9
|
+
export type DeviceOp = "create" | "exec" | "readFile" | "writeFile" | "stat" | "killTagged" | "killAll" | "stop";
|
|
10
|
+
/** First frame from the device after the WebSocket opens. */
|
|
11
|
+
export interface DeviceHello {
|
|
12
|
+
type: "hello";
|
|
13
|
+
protocol: number;
|
|
14
|
+
programVersion: string;
|
|
15
|
+
os: "darwin" | "linux";
|
|
16
|
+
arch: string;
|
|
17
|
+
mode: "os" | "docker";
|
|
18
|
+
roots: SandboxRoots;
|
|
19
|
+
sessionIsolation: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Error kinds a device reports for a failed request. */
|
|
22
|
+
export type DeviceErrorKind = "gone" | "fs_gone" | "policy" | "error";
|
|
23
|
+
/** The device's answer to a {@link KernelReq}. */
|
|
24
|
+
export type DeviceRes = {
|
|
25
|
+
type: "res";
|
|
26
|
+
id: string;
|
|
27
|
+
ok: true;
|
|
28
|
+
result: unknown;
|
|
29
|
+
} | {
|
|
30
|
+
type: "res";
|
|
31
|
+
id: string;
|
|
32
|
+
ok: false;
|
|
33
|
+
error: {
|
|
34
|
+
kind: DeviceErrorKind;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** A request from the kernel. */
|
|
39
|
+
export interface KernelReq {
|
|
40
|
+
type: "req";
|
|
41
|
+
id: string;
|
|
42
|
+
op: DeviceOp;
|
|
43
|
+
args: unknown;
|
|
44
|
+
deadlineMs: number;
|
|
45
|
+
}
|
|
46
|
+
/** The kernel gives up on a request (abort or deadline). */
|
|
47
|
+
export interface KernelCancel {
|
|
48
|
+
type: "cancel";
|
|
49
|
+
id: string;
|
|
50
|
+
}
|
|
51
|
+
export type DeviceFrame = DeviceHello | DeviceRes | KernelReq | KernelCancel;
|
|
52
|
+
export interface DeviceExecArgs {
|
|
53
|
+
command: string;
|
|
54
|
+
cwd?: string;
|
|
55
|
+
env?: Record<string, string>;
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
maxOutputBytes: number;
|
|
58
|
+
tag: string;
|
|
59
|
+
actor: SandboxActor;
|
|
60
|
+
limits: SandboxLimits;
|
|
61
|
+
}
|
|
62
|
+
export type DeviceExecResult = ExecResult;
|
|
63
|
+
export interface DeviceReadFileArgs {
|
|
64
|
+
path: string;
|
|
65
|
+
maxBytes: number;
|
|
66
|
+
}
|
|
67
|
+
export type DeviceReadFileResult = {
|
|
68
|
+
dataB64: string;
|
|
69
|
+
truncated: boolean;
|
|
70
|
+
} | null;
|
|
71
|
+
export interface DeviceWriteFileArgs {
|
|
72
|
+
path: string;
|
|
73
|
+
dataB64: string;
|
|
74
|
+
}
|
|
75
|
+
export type DeviceWriteFileResult = null;
|
|
76
|
+
export interface DeviceStatArgs {
|
|
77
|
+
path: string;
|
|
78
|
+
}
|
|
79
|
+
export type DeviceStatResult = {
|
|
80
|
+
size: number;
|
|
81
|
+
isDir: boolean;
|
|
82
|
+
} | null;
|
|
83
|
+
export interface DeviceKillTaggedArgs {
|
|
84
|
+
tags: string[];
|
|
85
|
+
}
|
|
86
|
+
export type DeviceKillAllArgs = Record<string, never>;
|
|
87
|
+
export interface DeviceCreateArgs {
|
|
88
|
+
limits: SandboxLimits;
|
|
89
|
+
}
|
|
90
|
+
export interface DeviceCreateResult {
|
|
91
|
+
roots: SandboxRoots;
|
|
92
|
+
}
|
|
93
|
+
export interface DeviceStopArgs {
|
|
94
|
+
reason: "revoked" | "deleted";
|
|
95
|
+
}
|
|
96
|
+
/** A frame that is not valid protocol v1 (or came from the wrong side). */
|
|
97
|
+
export declare class DeviceProtocolError extends Error {
|
|
98
|
+
constructor(message: string);
|
|
99
|
+
}
|
|
100
|
+
/** Result type per op. */
|
|
101
|
+
export interface DeviceOpResults {
|
|
102
|
+
create: DeviceCreateResult;
|
|
103
|
+
exec: DeviceExecResult;
|
|
104
|
+
readFile: DeviceReadFileResult;
|
|
105
|
+
writeFile: DeviceWriteFileResult;
|
|
106
|
+
stat: DeviceStatResult;
|
|
107
|
+
killTagged: null;
|
|
108
|
+
killAll: null;
|
|
109
|
+
stop: null;
|
|
110
|
+
}
|
|
111
|
+
/** Validates the `result` of a successful `res` for `op`; throws {@link DeviceProtocolError}. */
|
|
112
|
+
export declare function parseOpResult<Op extends DeviceOp>(op: Op, result: unknown): DeviceOpResults[Op];
|
|
113
|
+
/**
|
|
114
|
+
* Parses one WebSocket text frame. `from` is the sender: a device may send `hello` and `res`, the
|
|
115
|
+
* kernel `req` and `cancel`. Throws {@link DeviceProtocolError} on bad JSON, an unknown `type`,
|
|
116
|
+
* missing fields or a frame of the wrong direction.
|
|
117
|
+
*/
|
|
118
|
+
export declare function parseDeviceFrame(raw: string, from: "device" | "kernel"): DeviceFrame;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device protocol v1: the WebSocket frames between the kernel and a `capybara-device` program
|
|
3
|
+
* (a connected machine that serves as an environment). Pure types and hand-written guards.
|
|
4
|
+
*/
|
|
5
|
+
import { validateSandboxPathShape, } from "./sandbox.js";
|
|
6
|
+
/** The protocol version carried in `X-Capybara-Device-Protocol` and the hello frame. */
|
|
7
|
+
export const DEVICE_PROTOCOL_VERSION = 1;
|
|
8
|
+
const DEVICE_OPS = [
|
|
9
|
+
"create",
|
|
10
|
+
"exec",
|
|
11
|
+
"readFile",
|
|
12
|
+
"writeFile",
|
|
13
|
+
"stat",
|
|
14
|
+
"killTagged",
|
|
15
|
+
"killAll",
|
|
16
|
+
"stop",
|
|
17
|
+
];
|
|
18
|
+
/** A frame that is not valid protocol v1 (or came from the wrong side). */
|
|
19
|
+
export class DeviceProtocolError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "DeviceProtocolError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isRecord(v) {
|
|
26
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
27
|
+
}
|
|
28
|
+
function str(o, k) {
|
|
29
|
+
const v = o[k];
|
|
30
|
+
if (typeof v !== "string")
|
|
31
|
+
throw new DeviceProtocolError(`field "${k}" must be a string`);
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
function nonEmptyStr(o, k) {
|
|
35
|
+
const v = str(o, k);
|
|
36
|
+
if (v === "")
|
|
37
|
+
throw new DeviceProtocolError(`field "${k}" must not be empty`);
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
function posInt(o, k) {
|
|
41
|
+
const v = o[k];
|
|
42
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
|
|
43
|
+
throw new DeviceProtocolError(`field "${k}" must be a positive integer`);
|
|
44
|
+
}
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
function obj(v, what) {
|
|
48
|
+
if (!isRecord(v))
|
|
49
|
+
throw new DeviceProtocolError(`${what} must be an object`);
|
|
50
|
+
return v;
|
|
51
|
+
}
|
|
52
|
+
function bool(o, k) {
|
|
53
|
+
const v = o[k];
|
|
54
|
+
if (typeof v !== "boolean")
|
|
55
|
+
throw new DeviceProtocolError(`field "${k}" must be a boolean`);
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
function strArray(o, k) {
|
|
59
|
+
const v = o[k];
|
|
60
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
61
|
+
throw new DeviceProtocolError(`field "${k}" must be an array of strings`);
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
function parseLimits(v) {
|
|
66
|
+
const o = obj(v, 'field "limits"');
|
|
67
|
+
const egress = str(o, "egress");
|
|
68
|
+
if (egress !== "none" && egress !== "allowlist" && egress !== "open") {
|
|
69
|
+
throw new DeviceProtocolError(`unknown egress "${egress}"`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
cpu: num(o, "cpu"),
|
|
73
|
+
memoryMb: num(o, "memoryMb"),
|
|
74
|
+
diskMb: num(o, "diskMb"),
|
|
75
|
+
egress,
|
|
76
|
+
allowHosts: strArray(o, "allowHosts"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function num(o, k) {
|
|
80
|
+
const v = o[k];
|
|
81
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
82
|
+
throw new DeviceProtocolError(`field "${k}" must be a number`);
|
|
83
|
+
}
|
|
84
|
+
return v;
|
|
85
|
+
}
|
|
86
|
+
function parseRoots(v) {
|
|
87
|
+
if (!isRecord(v))
|
|
88
|
+
throw new DeviceProtocolError('field "roots" must be an object');
|
|
89
|
+
const roots = {};
|
|
90
|
+
for (const k of ["workspace", "skills", "tmp"]) {
|
|
91
|
+
const p = str(v, k);
|
|
92
|
+
if (!p.startsWith("/")) {
|
|
93
|
+
throw new DeviceProtocolError(`root "${k}" must be an absolute path`);
|
|
94
|
+
}
|
|
95
|
+
// the kernel builds paths as `${root}/…` and checks membership by prefix: a root must be a
|
|
96
|
+
// clean absolute directory path
|
|
97
|
+
const shape = validateSandboxPathShape(p);
|
|
98
|
+
if (shape.length > 0)
|
|
99
|
+
throw new DeviceProtocolError(`root "${k}": ${shape.join("; ")}`);
|
|
100
|
+
if (p.endsWith("/")) {
|
|
101
|
+
throw new DeviceProtocolError(`root "${k}" must not end with a slash`);
|
|
102
|
+
}
|
|
103
|
+
roots[k] = p;
|
|
104
|
+
}
|
|
105
|
+
return roots;
|
|
106
|
+
}
|
|
107
|
+
function parseHello(o) {
|
|
108
|
+
const os = str(o, "os");
|
|
109
|
+
if (os !== "darwin" && os !== "linux")
|
|
110
|
+
throw new DeviceProtocolError(`unsupported os "${os}"`);
|
|
111
|
+
const mode = str(o, "mode");
|
|
112
|
+
if (mode !== "os" && mode !== "docker") {
|
|
113
|
+
throw new DeviceProtocolError(`unsupported mode "${mode}"`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof o.sessionIsolation !== "boolean") {
|
|
116
|
+
throw new DeviceProtocolError('field "sessionIsolation" must be a boolean');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
type: "hello",
|
|
120
|
+
protocol: posInt(o, "protocol"),
|
|
121
|
+
programVersion: str(o, "programVersion"),
|
|
122
|
+
os,
|
|
123
|
+
arch: str(o, "arch"),
|
|
124
|
+
mode,
|
|
125
|
+
roots: parseRoots(o.roots),
|
|
126
|
+
sessionIsolation: o.sessionIsolation,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function parseRes(o) {
|
|
130
|
+
const id = nonEmptyStr(o, "id");
|
|
131
|
+
if (o.ok === true)
|
|
132
|
+
return { type: "res", id, ok: true, result: o.result };
|
|
133
|
+
if (o.ok !== false)
|
|
134
|
+
throw new DeviceProtocolError('field "ok" must be a boolean');
|
|
135
|
+
const e = o.error;
|
|
136
|
+
if (!isRecord(e))
|
|
137
|
+
throw new DeviceProtocolError('field "error" must be an object');
|
|
138
|
+
const kind = str(e, "kind");
|
|
139
|
+
if (kind !== "gone" && kind !== "fs_gone" && kind !== "policy" && kind !== "error") {
|
|
140
|
+
throw new DeviceProtocolError(`unknown error kind "${kind}"`);
|
|
141
|
+
}
|
|
142
|
+
return { type: "res", id, ok: false, error: { kind, message: str(e, "message") } };
|
|
143
|
+
}
|
|
144
|
+
function parseReq(o) {
|
|
145
|
+
const op = str(o, "op");
|
|
146
|
+
if (!DEVICE_OPS.includes(op))
|
|
147
|
+
throw new DeviceProtocolError(`unknown op "${op}"`);
|
|
148
|
+
const args = obj(o.args, 'field "args"');
|
|
149
|
+
validateArgs(op, args);
|
|
150
|
+
return {
|
|
151
|
+
type: "req",
|
|
152
|
+
id: nonEmptyStr(o, "id"),
|
|
153
|
+
op: op,
|
|
154
|
+
args,
|
|
155
|
+
deadlineMs: posInt(o, "deadlineMs"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function validateArgs(op, a) {
|
|
159
|
+
switch (op) {
|
|
160
|
+
case "exec": {
|
|
161
|
+
nonEmptyStr(a, "command");
|
|
162
|
+
posInt(a, "timeoutMs");
|
|
163
|
+
posInt(a, "maxOutputBytes");
|
|
164
|
+
str(a, "tag");
|
|
165
|
+
const actor = obj(a.actor, 'field "actor"');
|
|
166
|
+
str(actor, "accountId");
|
|
167
|
+
str(actor, "conversationId");
|
|
168
|
+
parseLimits(a.limits);
|
|
169
|
+
if (a.cwd !== undefined)
|
|
170
|
+
str(a, "cwd");
|
|
171
|
+
if (a.env !== undefined) {
|
|
172
|
+
const env = obj(a.env, 'field "env"');
|
|
173
|
+
for (const k of Object.keys(env))
|
|
174
|
+
str(env, k);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case "readFile": {
|
|
179
|
+
str(a, "path");
|
|
180
|
+
const m = a.maxBytes;
|
|
181
|
+
if (typeof m !== "number" || !Number.isInteger(m) || m < 0) {
|
|
182
|
+
throw new DeviceProtocolError('field "maxBytes" must be a non-negative integer');
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case "writeFile":
|
|
187
|
+
str(a, "path");
|
|
188
|
+
str(a, "dataB64");
|
|
189
|
+
return;
|
|
190
|
+
case "stat":
|
|
191
|
+
str(a, "path");
|
|
192
|
+
return;
|
|
193
|
+
case "killTagged":
|
|
194
|
+
strArray(a, "tags");
|
|
195
|
+
return;
|
|
196
|
+
case "killAll":
|
|
197
|
+
return;
|
|
198
|
+
case "create":
|
|
199
|
+
parseLimits(a.limits);
|
|
200
|
+
return;
|
|
201
|
+
case "stop": {
|
|
202
|
+
const r = str(a, "reason");
|
|
203
|
+
if (r !== "revoked" && r !== "deleted") {
|
|
204
|
+
throw new DeviceProtocolError(`unknown stop reason "${r}"`);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function parseOpResult(op, result) {
|
|
211
|
+
switch (op) {
|
|
212
|
+
case "exec": {
|
|
213
|
+
const r = obj(result, "exec result");
|
|
214
|
+
const exitCode = r.exitCode;
|
|
215
|
+
if (exitCode !== null && (typeof exitCode !== "number" || !Number.isFinite(exitCode))) {
|
|
216
|
+
throw new DeviceProtocolError('field "exitCode" must be a number or null');
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
exitCode: exitCode,
|
|
220
|
+
stdout: str(r, "stdout"),
|
|
221
|
+
stderr: str(r, "stderr"),
|
|
222
|
+
truncated: bool(r, "truncated"),
|
|
223
|
+
timedOut: bool(r, "timedOut"),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
case "readFile": {
|
|
227
|
+
if (result === null)
|
|
228
|
+
return null;
|
|
229
|
+
const r = obj(result, "readFile result");
|
|
230
|
+
return { dataB64: str(r, "dataB64"), truncated: bool(r, "truncated") };
|
|
231
|
+
}
|
|
232
|
+
case "stat": {
|
|
233
|
+
if (result === null)
|
|
234
|
+
return null;
|
|
235
|
+
const r = obj(result, "stat result");
|
|
236
|
+
return { size: num(r, "size"), isDir: bool(r, "isDir") };
|
|
237
|
+
}
|
|
238
|
+
case "create":
|
|
239
|
+
return { roots: parseRoots(obj(result, "create result").roots) };
|
|
240
|
+
default:
|
|
241
|
+
if (result !== null)
|
|
242
|
+
throw new DeviceProtocolError(`${op} result must be null`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Parses one WebSocket text frame. `from` is the sender: a device may send `hello` and `res`, the
|
|
248
|
+
* kernel `req` and `cancel`. Throws {@link DeviceProtocolError} on bad JSON, an unknown `type`,
|
|
249
|
+
* missing fields or a frame of the wrong direction.
|
|
250
|
+
*/
|
|
251
|
+
export function parseDeviceFrame(raw, from) {
|
|
252
|
+
let o;
|
|
253
|
+
try {
|
|
254
|
+
o = JSON.parse(raw);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
throw new DeviceProtocolError("frame is not valid JSON");
|
|
258
|
+
}
|
|
259
|
+
if (!isRecord(o))
|
|
260
|
+
throw new DeviceProtocolError("frame must be a JSON object");
|
|
261
|
+
const type = o.type;
|
|
262
|
+
switch (type) {
|
|
263
|
+
case "hello":
|
|
264
|
+
case "res":
|
|
265
|
+
if (from !== "device")
|
|
266
|
+
throw new DeviceProtocolError(`"${type}" must come from the device`);
|
|
267
|
+
return type === "hello" ? parseHello(o) : parseRes(o);
|
|
268
|
+
case "req":
|
|
269
|
+
case "cancel":
|
|
270
|
+
if (from !== "kernel")
|
|
271
|
+
throw new DeviceProtocolError(`"${type}" must come from the kernel`);
|
|
272
|
+
return type === "req" ? parseReq(o) : { type: "cancel", id: nonEmptyStr(o, "id") };
|
|
273
|
+
default:
|
|
274
|
+
throw new DeviceProtocolError(`unknown frame type "${String(type)}"`);
|
|
275
|
+
}
|
|
276
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.1.0-rc.
|
|
1
|
+
export declare const SDK_VERSION = "0.1.0-rc.7";
|
|
2
2
|
export * from "./agent-template.js";
|
|
3
3
|
export * from "./bootstrap.js";
|
|
4
4
|
export * from "./cards.js";
|
|
@@ -8,6 +8,7 @@ export * from "./connector.js";
|
|
|
8
8
|
export * from "./connector-auth.js";
|
|
9
9
|
export * from "./ctx.js";
|
|
10
10
|
export * from "./define-extension.js";
|
|
11
|
+
export * from "./device-protocol.js";
|
|
11
12
|
export * from "./directory.js";
|
|
12
13
|
export * from "./extension-point.js";
|
|
13
14
|
export * from "./file-preview.js";
|
|
@@ -22,6 +23,7 @@ export * from "./plugin-manifest.js";
|
|
|
22
23
|
export * from "./plugin-package.js";
|
|
23
24
|
export * from "./sandbox.js";
|
|
24
25
|
export * from "./secret.js";
|
|
26
|
+
export * from "./setup-guide.js";
|
|
25
27
|
export * from "./skill.js";
|
|
26
28
|
export * from "./skill-invocation.js";
|
|
27
29
|
export * from "./storage.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.1.0-rc.
|
|
1
|
+
export const SDK_VERSION = "0.1.0-rc.7";
|
|
2
2
|
export * from "./agent-template.js";
|
|
3
3
|
export * from "./bootstrap.js";
|
|
4
4
|
export * from "./cards.js";
|
|
@@ -8,6 +8,7 @@ export * from "./connector.js";
|
|
|
8
8
|
export * from "./connector-auth.js";
|
|
9
9
|
export * from "./ctx.js";
|
|
10
10
|
export * from "./define-extension.js";
|
|
11
|
+
export * from "./device-protocol.js";
|
|
11
12
|
export * from "./directory.js";
|
|
12
13
|
export * from "./extension-point.js";
|
|
13
14
|
export * from "./file-preview.js";
|
|
@@ -22,6 +23,7 @@ export * from "./plugin-manifest.js";
|
|
|
22
23
|
export * from "./plugin-package.js";
|
|
23
24
|
export * from "./sandbox.js";
|
|
24
25
|
export * from "./secret.js";
|
|
26
|
+
export * from "./setup-guide.js";
|
|
25
27
|
export * from "./skill.js";
|
|
26
28
|
export * from "./skill-invocation.js";
|
|
27
29
|
export * from "./storage.js";
|
package/dist/marketplace.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare const MARKETPLACE_LIMITS: {
|
|
|
28
28
|
readonly plugins: 5000;
|
|
29
29
|
readonly permissionKeys: 500;
|
|
30
30
|
readonly urlChars: 2000;
|
|
31
|
+
/** Bytes of an entry icon (the plugin icon limit, {@link CONNECTOR_TEMPLATE_LIMITS}). */
|
|
32
|
+
readonly iconBytes: number;
|
|
31
33
|
};
|
|
32
34
|
/** Where a marketplace entry's plugin comes from. */
|
|
33
35
|
export type MarketplaceSource = {
|
|
@@ -75,6 +77,18 @@ export interface MarketplaceEntry {
|
|
|
75
77
|
};
|
|
76
78
|
/** https URL. */
|
|
77
79
|
homepage?: string;
|
|
80
|
+
/**
|
|
81
|
+
* The plugin's icon for marketplace lists, before the plugin is imported (display only; an
|
|
82
|
+
* imported plugin shows its own icon). Either an absolute `https://` URL whose path ends in
|
|
83
|
+
* `.svg` / `.png`, or `./<path>.svg|png` inside the marketplace's repository, read at the commit
|
|
84
|
+
* the marketplace was refreshed at (git marketplaces only; ignored in a marketplace fetched over
|
|
85
|
+
* http). The kernel downloads it on refresh (network rules of the marketplace downloads, never
|
|
86
|
+
* with the marketplace token), keeps it when it really is an svg / png of at most
|
|
87
|
+
* {@link MARKETPLACE_LIMITS}`.iconBytes`, and treats a URL's content as fixed: a new icon needs a
|
|
88
|
+
* new URL. A missing or invalid icon never fails the refresh. `capybara-plugin marketplace
|
|
89
|
+
* --write` fills it with the repository path of the plugin's `plugin.json` `icon`.
|
|
90
|
+
*/
|
|
91
|
+
icon?: string;
|
|
78
92
|
/**
|
|
79
93
|
* This version is deprecated (`true`, or with a reason shown to admins): browsing lists it under
|
|
80
94
|
* the plugin's other versions, never as the one to import, and the kernel refuses importing it
|
|
@@ -103,6 +117,8 @@ export interface MarketplaceParseOptions {
|
|
|
103
117
|
/** Tests only: allow http on loopback addresses for `git` / `archive` URLs. */
|
|
104
118
|
allowInsecureLoopback?: boolean;
|
|
105
119
|
}
|
|
120
|
+
/** An entry icon's file extension (of the URL's path, or of the repository path). */
|
|
121
|
+
export declare const MARKETPLACE_ICON_RE: RegExp;
|
|
106
122
|
/**
|
|
107
123
|
* The zod schema of `marketplace.json` (https only; non-strict: unknown fields dropped). The JSON
|
|
108
124
|
* Schema is generated with `z.toJSONSchema(MarketplaceManifestSchema, { io: "input" })`.
|
|
@@ -117,6 +133,16 @@ export declare function parseMarketplaceManifest(json: unknown, opts?: Marketpla
|
|
|
117
133
|
};
|
|
118
134
|
/** The entry's version: `version`, else the npm source's version, else `null` (the plugin's own). */
|
|
119
135
|
export declare function marketplaceEntryVersion(e: MarketplaceEntry): string | null;
|
|
136
|
+
/**
|
|
137
|
+
* The entry's icon (see {@link MarketplaceEntry.icon}): `{ url }` for an absolute URL, `{ path }`
|
|
138
|
+
* (normalized, without the leading `./`) for a path inside the marketplace's repository, `null`
|
|
139
|
+
* without one.
|
|
140
|
+
*/
|
|
141
|
+
export declare function marketplaceEntryIcon(e: Pick<MarketplaceEntry, "icon">): {
|
|
142
|
+
url: string;
|
|
143
|
+
} | {
|
|
144
|
+
path: string;
|
|
145
|
+
} | null;
|
|
120
146
|
/**
|
|
121
147
|
* The entry's deprecation (see {@link MarketplaceEntry.deprecated}): `null` when it is not
|
|
122
148
|
* deprecated (absent or `false`), else its reason (`null` without one).
|