@tangle-network/agent-interface 0.10.1 → 0.11.1
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/LICENSE +18 -8
- package/README.md +31 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2 -0
- package/dist/profile-schema.d.ts +38 -15
- package/dist/profile-schema.js +6 -0
- package/dist/profile-security.d.ts +71 -0
- package/dist/profile-security.js +179 -0
- package/dist/sandbox-size.d.ts +22 -0
- package/dist/sandbox-size.js +21 -0
- package/package.json +15 -2
package/LICENSE
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Copyright (c) 2026 Tangle Network
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @tangle-network/agent-interface
|
|
2
|
+
|
|
3
|
+
Shared TypeScript types and zod schemas that define the contract between Tangle
|
|
4
|
+
agents, the sidecar, and provider adapters: capabilities, agent profiles,
|
|
5
|
+
message parts, and harness descriptors. This is the canonical home for those
|
|
6
|
+
shapes; higher-level packages import from here rather than redefining them.
|
|
7
|
+
|
|
8
|
+
The only runtime dependency is `zod` (used for the schema exports).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @tangle-network/agent-interface
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import type { BackendCapabilities, ProviderCapabilities } from "@tangle-network/agent-interface";
|
|
20
|
+
|
|
21
|
+
const caps: ProviderCapabilities = {
|
|
22
|
+
supportsVision: true,
|
|
23
|
+
supportsLogprobs: false,
|
|
24
|
+
supportsToolCalls: true,
|
|
25
|
+
supportsComputerUse: false,
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,13 @@ export type ToolInvocation = {
|
|
|
214
214
|
toolName: string;
|
|
215
215
|
input: unknown;
|
|
216
216
|
result?: unknown;
|
|
217
|
+
/**
|
|
218
|
+
* True when the tool call failed (errored, timed out, or was rejected).
|
|
219
|
+
* Failed tools are recorded — not dropped — so the run outcome can reflect
|
|
220
|
+
* them. Consumers deriving success must treat any `isError: true` invocation
|
|
221
|
+
* as a failure signal.
|
|
222
|
+
*/
|
|
223
|
+
isError?: boolean;
|
|
217
224
|
};
|
|
218
225
|
export type TokenUsage = {
|
|
219
226
|
inputTokens: number;
|
|
@@ -582,3 +589,5 @@ export * from "./agent-profile.js";
|
|
|
582
589
|
export * from "./harness.js";
|
|
583
590
|
export * from "./harness-capabilities.js";
|
|
584
591
|
export * from "./profile-schema.js";
|
|
592
|
+
export * from "./profile-security.js";
|
|
593
|
+
export * from "./sandbox-size.js";
|
package/dist/index.js
CHANGED
package/dist/profile-schema.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { AgentProfile } from "./agent-profile.js";
|
|
3
|
+
import { type SandboxSizePreset } from "./sandbox-size.js";
|
|
2
4
|
export declare const agentProfilePermissionValueSchema: z.ZodEnum<{
|
|
3
5
|
allow: "allow";
|
|
4
|
-
ask: "ask";
|
|
5
6
|
deny: "deny";
|
|
7
|
+
ask: "ask";
|
|
6
8
|
}>;
|
|
7
9
|
export declare const agentProfilePermissionSchema: z.ZodUnion<readonly [z.ZodEnum<{
|
|
8
10
|
allow: "allow";
|
|
9
|
-
ask: "ask";
|
|
10
11
|
deny: "deny";
|
|
12
|
+
ask: "ask";
|
|
11
13
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
12
14
|
allow: "allow";
|
|
13
|
-
ask: "ask";
|
|
14
15
|
deny: "deny";
|
|
16
|
+
ask: "ask";
|
|
15
17
|
}>>]>;
|
|
16
18
|
export declare const agentProfileResourceRefSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
17
19
|
kind: z.ZodLiteral<"inline">;
|
|
@@ -117,10 +119,10 @@ export declare const agentProfileModelHintsSchema: z.ZodObject<{
|
|
|
117
119
|
small: z.ZodOptional<z.ZodString>;
|
|
118
120
|
provider: z.ZodOptional<z.ZodString>;
|
|
119
121
|
reasoningEffort: z.ZodOptional<z.ZodEnum<{
|
|
122
|
+
medium: "medium";
|
|
120
123
|
none: "none";
|
|
121
124
|
minimal: "minimal";
|
|
122
125
|
low: "low";
|
|
123
|
-
medium: "medium";
|
|
124
126
|
high: "high";
|
|
125
127
|
xhigh: "xhigh";
|
|
126
128
|
ultracode: "ultracode";
|
|
@@ -138,12 +140,12 @@ export declare const agentSubagentProfileSchema: z.ZodObject<{
|
|
|
138
140
|
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
139
141
|
permissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
140
142
|
allow: "allow";
|
|
141
|
-
ask: "ask";
|
|
142
143
|
deny: "deny";
|
|
144
|
+
ask: "ask";
|
|
143
145
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
144
146
|
allow: "allow";
|
|
145
|
-
ask: "ask";
|
|
146
147
|
deny: "deny";
|
|
148
|
+
ask: "ask";
|
|
147
149
|
}>>]>>>;
|
|
148
150
|
maxSteps: z.ZodOptional<z.ZodNumber>;
|
|
149
151
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -162,12 +164,12 @@ export declare const agentProfileModeSchema: z.ZodObject<{
|
|
|
162
164
|
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
163
165
|
permissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
164
166
|
allow: "allow";
|
|
165
|
-
ask: "ask";
|
|
166
167
|
deny: "deny";
|
|
168
|
+
ask: "ask";
|
|
167
169
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
168
170
|
allow: "allow";
|
|
169
|
-
ask: "ask";
|
|
170
171
|
deny: "deny";
|
|
172
|
+
ask: "ask";
|
|
171
173
|
}>>]>>>;
|
|
172
174
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
173
175
|
}, z.core.$strip>;
|
|
@@ -216,10 +218,10 @@ export declare const agentProfileSchema: z.ZodObject<{
|
|
|
216
218
|
small: z.ZodOptional<z.ZodString>;
|
|
217
219
|
provider: z.ZodOptional<z.ZodString>;
|
|
218
220
|
reasoningEffort: z.ZodOptional<z.ZodEnum<{
|
|
221
|
+
medium: "medium";
|
|
219
222
|
none: "none";
|
|
220
223
|
minimal: "minimal";
|
|
221
224
|
low: "low";
|
|
222
|
-
medium: "medium";
|
|
223
225
|
high: "high";
|
|
224
226
|
xhigh: "xhigh";
|
|
225
227
|
ultracode: "ultracode";
|
|
@@ -228,12 +230,12 @@ export declare const agentProfileSchema: z.ZodObject<{
|
|
|
228
230
|
}, z.core.$strip>>;
|
|
229
231
|
permissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
230
232
|
allow: "allow";
|
|
231
|
-
ask: "ask";
|
|
232
233
|
deny: "deny";
|
|
234
|
+
ask: "ask";
|
|
233
235
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
234
236
|
allow: "allow";
|
|
235
|
-
ask: "ask";
|
|
236
237
|
deny: "deny";
|
|
238
|
+
ask: "ask";
|
|
237
239
|
}>>]>>>;
|
|
238
240
|
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
239
241
|
mcp: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -263,12 +265,12 @@ export declare const agentProfileSchema: z.ZodObject<{
|
|
|
263
265
|
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
264
266
|
permissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
265
267
|
allow: "allow";
|
|
266
|
-
ask: "ask";
|
|
267
268
|
deny: "deny";
|
|
269
|
+
ask: "ask";
|
|
268
270
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
269
271
|
allow: "allow";
|
|
270
|
-
ask: "ask";
|
|
271
272
|
deny: "deny";
|
|
273
|
+
ask: "ask";
|
|
272
274
|
}>>]>>>;
|
|
273
275
|
maxSteps: z.ZodOptional<z.ZodNumber>;
|
|
274
276
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -360,12 +362,12 @@ export declare const agentProfileSchema: z.ZodObject<{
|
|
|
360
362
|
tools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
361
363
|
permissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
362
364
|
allow: "allow";
|
|
363
|
-
ask: "ask";
|
|
364
365
|
deny: "deny";
|
|
366
|
+
ask: "ask";
|
|
365
367
|
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
366
368
|
allow: "allow";
|
|
367
|
-
ask: "ask";
|
|
368
369
|
deny: "deny";
|
|
370
|
+
ask: "ask";
|
|
369
371
|
}>>]>>>;
|
|
370
372
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
371
373
|
}, z.core.$strip>>>;
|
|
@@ -378,3 +380,24 @@ export declare const agentProfileSchema: z.ZodObject<{
|
|
|
378
380
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
379
381
|
extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodUndefined]>>>;
|
|
380
382
|
}, z.core.$strip>;
|
|
383
|
+
/**
|
|
384
|
+
* A registered capability: a stable id paired with its canonical
|
|
385
|
+
* {@link AgentProfile}. `definition` is the full profile (prompt/model/tools/
|
|
386
|
+
* mcp/permissions), so a capability carries the whole agent shape, not just a
|
|
387
|
+
* system prompt. The platform capability registry validates and stores these.
|
|
388
|
+
*/
|
|
389
|
+
export interface Capability {
|
|
390
|
+
/** Stable, deterministic id — the key a workflow's `agent.run.profile` names. */
|
|
391
|
+
id: string;
|
|
392
|
+
/** The canonical agent profile (prompt/model/tools/mcp/permissions). */
|
|
393
|
+
definition: AgentProfile;
|
|
394
|
+
/**
|
|
395
|
+
* Recommended compute tier for a sandbox running this capability. A dispatcher
|
|
396
|
+
* uses it as the size DEFAULT when the caller does not pick one — so a
|
|
397
|
+
* capability that only ever does thin work defaults to a small box instead of
|
|
398
|
+
* a maxed one. The caller may always override it per dispatch. Omitted → the
|
|
399
|
+
* dispatcher's own default tier.
|
|
400
|
+
*/
|
|
401
|
+
recommendedSize?: SandboxSizePreset;
|
|
402
|
+
}
|
|
403
|
+
export declare const capabilitySchema: z.ZodType<Capability>;
|
package/dist/profile-schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { SANDBOX_SIZE_PRESET_NAMES, } from "./sandbox-size.js";
|
|
2
3
|
export const agentProfilePermissionValueSchema = z.enum([
|
|
3
4
|
"allow",
|
|
4
5
|
"deny",
|
|
@@ -126,3 +127,8 @@ export const agentProfileSchema = z.object({
|
|
|
126
127
|
});
|
|
127
128
|
const _agentProfileSchemaMatchesInterface = true;
|
|
128
129
|
void _agentProfileSchemaMatchesInterface;
|
|
130
|
+
export const capabilitySchema = z.object({
|
|
131
|
+
id: z.string().min(1),
|
|
132
|
+
definition: agentProfileSchema,
|
|
133
|
+
recommendedSize: z.enum(SANDBOX_SIZE_PRESET_NAMES).optional(),
|
|
134
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security validation for inline {@link AgentProfile} values.
|
|
3
|
+
*
|
|
4
|
+
* When a caller sends a full profile inline (rather than naming a curated,
|
|
5
|
+
* trusted capability), the profile is author-controlled and may declare surfaces
|
|
6
|
+
* that execute code OUTSIDE the agent's own reasoning — a stdio/local MCP server
|
|
7
|
+
* (an arbitrary command spawned at startup) or a hook (a shell command run
|
|
8
|
+
* automatically around the turn). A sandbox isolates the workload, but these
|
|
9
|
+
* surfaces run unattended on the owner's behalf, so a cloud dispatcher must gate
|
|
10
|
+
* them before materializing the profile.
|
|
11
|
+
*
|
|
12
|
+
* This validates the CANONICAL agent-interface shape (`mcp` keyed by
|
|
13
|
+
* `AgentProfileMcpServer`, `hooks` keyed by `AgentProfileHookCommand[]`) — the
|
|
14
|
+
* provider-neutral contract every backend translates from. The opencode-native
|
|
15
|
+
* `validateProfileSecurity` (in `sdk-provider-opencode`) operates on opencode's
|
|
16
|
+
* own profile shape and is a different layer; this is the one to use at the
|
|
17
|
+
* application boundary where profiles are still provider-neutral.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentProfile, AgentProfileValidationResult } from "./agent-profile.js";
|
|
20
|
+
/** Policy for {@link validateAgentProfileSecurity}. */
|
|
21
|
+
export interface AgentProfileSecurityPolicy {
|
|
22
|
+
/**
|
|
23
|
+
* Allow stdio/local MCP servers (a spawned local process). Off in cloud: an
|
|
24
|
+
* inline profile must not start arbitrary commands at MCP init.
|
|
25
|
+
*/
|
|
26
|
+
allowLocalMcp: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Allow lifecycle hooks (shell commands run automatically around the turn).
|
|
29
|
+
* Off in cloud: hooks are author-controlled shell outside the agent's loop.
|
|
30
|
+
*/
|
|
31
|
+
allowHooks: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Glob allowlist for REMOTE MCP hosts (http/sse `url` hostnames). A set list
|
|
34
|
+
* rejects any host that matches no pattern; an empty list (`[]`) blocks ALL
|
|
35
|
+
* remote MCP.
|
|
36
|
+
*
|
|
37
|
+
* SECURITY: `undefined` leaves remote MCP hosts UNRESTRICTED — it does NOT
|
|
38
|
+
* protect against SSRF (an inline profile could point an MCP server at, e.g.,
|
|
39
|
+
* `http://169.254.169.254/` cloud metadata or an internal address). When this
|
|
40
|
+
* is `undefined`, the caller MUST enforce SSRF/egress controls at the network
|
|
41
|
+
* layer, or set a restrictive allowlist (or `[]`) to fail closed.
|
|
42
|
+
*/
|
|
43
|
+
allowedMcpHosts?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Allow `connections` (hub-managed integration grants). `undefined` leaves them
|
|
46
|
+
* allowed — a connection is a legitimate inline-profile feature where the host
|
|
47
|
+
* wires it. A host that does NOT support inline connection grants (e.g. a
|
|
48
|
+
* surface that grants hub access through a separate, audited path) sets this
|
|
49
|
+
* `false` so an inline profile cannot smuggle hub access through the profile.
|
|
50
|
+
*/
|
|
51
|
+
allowConnections?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Default cloud policy: block the two unattended-code surfaces (local MCP,
|
|
55
|
+
* hooks); leave remote MCP and everything else to the profile. Deliberately
|
|
56
|
+
* narrow — it gates code execution paths, not the agent's normal tools/edits,
|
|
57
|
+
* which the sandbox already isolates.
|
|
58
|
+
*
|
|
59
|
+
* NOTE: this default leaves `allowedMcpHosts` undefined, so REMOTE MCP hosts are
|
|
60
|
+
* unrestricted — it is NOT an SSRF guard (see `allowedMcpHosts`). A surface that
|
|
61
|
+
* must fail closed against arbitrary MCP egress should set `allowedMcpHosts`
|
|
62
|
+
* (e.g. `[]` to block all remote MCP), as the workflow inline-profile policy does.
|
|
63
|
+
*/
|
|
64
|
+
export declare const DEFAULT_CLOUD_AGENT_PROFILE_SECURITY_POLICY: AgentProfileSecurityPolicy;
|
|
65
|
+
/**
|
|
66
|
+
* Validate an inline profile against a security policy. Returns `ok: false` with
|
|
67
|
+
* `error`-level issues when the profile declares a blocked surface; warnings do
|
|
68
|
+
* not fail. Pure and synchronous — safe to run at author (compile) time and
|
|
69
|
+
* again at dispatch as defense in depth.
|
|
70
|
+
*/
|
|
71
|
+
export declare function validateAgentProfileSecurity(profile: AgentProfile, policy?: AgentProfileSecurityPolicy): AgentProfileValidationResult;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security validation for inline {@link AgentProfile} values.
|
|
3
|
+
*
|
|
4
|
+
* When a caller sends a full profile inline (rather than naming a curated,
|
|
5
|
+
* trusted capability), the profile is author-controlled and may declare surfaces
|
|
6
|
+
* that execute code OUTSIDE the agent's own reasoning — a stdio/local MCP server
|
|
7
|
+
* (an arbitrary command spawned at startup) or a hook (a shell command run
|
|
8
|
+
* automatically around the turn). A sandbox isolates the workload, but these
|
|
9
|
+
* surfaces run unattended on the owner's behalf, so a cloud dispatcher must gate
|
|
10
|
+
* them before materializing the profile.
|
|
11
|
+
*
|
|
12
|
+
* This validates the CANONICAL agent-interface shape (`mcp` keyed by
|
|
13
|
+
* `AgentProfileMcpServer`, `hooks` keyed by `AgentProfileHookCommand[]`) — the
|
|
14
|
+
* provider-neutral contract every backend translates from. The opencode-native
|
|
15
|
+
* `validateProfileSecurity` (in `sdk-provider-opencode`) operates on opencode's
|
|
16
|
+
* own profile shape and is a different layer; this is the one to use at the
|
|
17
|
+
* application boundary where profiles are still provider-neutral.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Default cloud policy: block the two unattended-code surfaces (local MCP,
|
|
21
|
+
* hooks); leave remote MCP and everything else to the profile. Deliberately
|
|
22
|
+
* narrow — it gates code execution paths, not the agent's normal tools/edits,
|
|
23
|
+
* which the sandbox already isolates.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: this default leaves `allowedMcpHosts` undefined, so REMOTE MCP hosts are
|
|
26
|
+
* unrestricted — it is NOT an SSRF guard (see `allowedMcpHosts`). A surface that
|
|
27
|
+
* must fail closed against arbitrary MCP egress should set `allowedMcpHosts`
|
|
28
|
+
* (e.g. `[]` to block all remote MCP), as the workflow inline-profile policy does.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_CLOUD_AGENT_PROFILE_SECURITY_POLICY = {
|
|
31
|
+
allowLocalMcp: false,
|
|
32
|
+
allowHooks: false,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Match one DNS label against one pattern label, where `*` matches any run of
|
|
36
|
+
* characters WITHIN the label (never a `.`). Split-on-`*` with linear
|
|
37
|
+
* prefix/suffix/in-order scanning — deliberately NOT a constructed `RegExp`, so
|
|
38
|
+
* on this security boundary it stays provably linear with no catastrophic-
|
|
39
|
+
* backtracking surface whatever pattern an allowlist carries.
|
|
40
|
+
*/
|
|
41
|
+
function matchLabel(label, pattern) {
|
|
42
|
+
const segments = pattern.split("*");
|
|
43
|
+
if (segments.length === 1)
|
|
44
|
+
return label === segments[0]; // no wildcard
|
|
45
|
+
const first = segments[0];
|
|
46
|
+
const last = segments[segments.length - 1];
|
|
47
|
+
if (!label.startsWith(first) || !label.endsWith(last))
|
|
48
|
+
return false;
|
|
49
|
+
// Prefix and suffix may not overlap (e.g. `aa*aa` must not match `aaa`).
|
|
50
|
+
if (first.length + last.length > label.length)
|
|
51
|
+
return false;
|
|
52
|
+
let cursor = first.length;
|
|
53
|
+
const suffixStart = label.length - last.length;
|
|
54
|
+
for (let i = 1; i < segments.length - 1; i += 1) {
|
|
55
|
+
const seg = segments[i];
|
|
56
|
+
if (seg.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
const found = label.indexOf(seg, cursor);
|
|
59
|
+
if (found === -1 || found + seg.length > suffixStart)
|
|
60
|
+
return false;
|
|
61
|
+
cursor = found + seg.length;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Case-insensitive host glob (`*` only) for allowlists, matched at DNS LABEL
|
|
67
|
+
* granularity: the host and pattern are split on `.` and matched label-by-label,
|
|
68
|
+
* and a `*` matches within a single label only — it never crosses a `.`. So
|
|
69
|
+
* `*.example.com` matches `api.example.com` but NOT `evil-example.com` (no dot
|
|
70
|
+
* boundary), `a.b.example.com` (extra label), or the bare apex `example.com`
|
|
71
|
+
* (missing label). This prevents a wildcard from reaching an unintended sibling
|
|
72
|
+
* or deeper domain on the security boundary.
|
|
73
|
+
*
|
|
74
|
+
* An IPv6 host (the hostname contains `:`) is matched EXACTLY — never split or
|
|
75
|
+
* globbed — since `.`-label semantics don't apply to it (and an IPv4-mapped form
|
|
76
|
+
* like `::ffff:1.2.3.4` contains dots that would otherwise glob unpredictably).
|
|
77
|
+
*/
|
|
78
|
+
function matchHostGlob(host, pattern) {
|
|
79
|
+
const h = host.toLowerCase();
|
|
80
|
+
const p = pattern.toLowerCase();
|
|
81
|
+
if (h.includes(":") || p.includes(":"))
|
|
82
|
+
return h === p;
|
|
83
|
+
const hostLabels = h.split(".");
|
|
84
|
+
const patternLabels = p.split(".");
|
|
85
|
+
if (hostLabels.length !== patternLabels.length)
|
|
86
|
+
return false;
|
|
87
|
+
return patternLabels.every((label, i) => matchLabel(hostLabels[i], label));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A local/stdio MCP server spawns a process; a remote one connects over the
|
|
91
|
+
* network. ANY `command` makes it local — a spawnable process command is the
|
|
92
|
+
* thing being gated, and it stays dangerous whatever `transport` is declared
|
|
93
|
+
* alongside it (pairing `command` with `transport: "sse"`/`"http"` must not slip
|
|
94
|
+
* it past as "remote"). `transport: "stdio"` is local even with no command.
|
|
95
|
+
*/
|
|
96
|
+
function isLocalMcpServer(server) {
|
|
97
|
+
return server.command !== undefined || server.transport === "stdio";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validate an inline profile against a security policy. Returns `ok: false` with
|
|
101
|
+
* `error`-level issues when the profile declares a blocked surface; warnings do
|
|
102
|
+
* not fail. Pure and synchronous — safe to run at author (compile) time and
|
|
103
|
+
* again at dispatch as defense in depth.
|
|
104
|
+
*/
|
|
105
|
+
export function validateAgentProfileSecurity(profile, policy = DEFAULT_CLOUD_AGENT_PROFILE_SECURITY_POLICY) {
|
|
106
|
+
const issues = [];
|
|
107
|
+
for (const [name, server] of Object.entries(profile.mcp ?? {})) {
|
|
108
|
+
if (isLocalMcpServer(server)) {
|
|
109
|
+
if (!policy.allowLocalMcp) {
|
|
110
|
+
issues.push({
|
|
111
|
+
level: "error",
|
|
112
|
+
code: "BLOCKED_LOCAL_MCP",
|
|
113
|
+
message: `local/stdio MCP server '${name}' is not allowed (it spawns an arbitrary process)`,
|
|
114
|
+
path: `mcp.${name}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (policy.allowedMcpHosts) {
|
|
120
|
+
// An allowlist is active, so a non-local server MUST present a matchable
|
|
121
|
+
// host — a missing/empty `url` cannot be allowlist-checked, so it fails
|
|
122
|
+
// closed rather than slipping through unvalidated.
|
|
123
|
+
if (!server.url) {
|
|
124
|
+
issues.push({
|
|
125
|
+
level: "error",
|
|
126
|
+
code: "INVALID_MCP_URL",
|
|
127
|
+
message: `remote MCP server '${name}' has no url to check against the allowlist`,
|
|
128
|
+
path: `mcp.${name}`,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let host;
|
|
133
|
+
try {
|
|
134
|
+
host = new URL(server.url).hostname;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
issues.push({
|
|
138
|
+
level: "error",
|
|
139
|
+
code: "INVALID_MCP_URL",
|
|
140
|
+
message: `MCP server '${name}' has an invalid url: ${server.url}`,
|
|
141
|
+
path: `mcp.${name}`,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!policy.allowedMcpHosts.some((p) => matchHostGlob(host, p))) {
|
|
146
|
+
issues.push({
|
|
147
|
+
level: "error",
|
|
148
|
+
code: "BLOCKED_REMOTE_MCP_HOST",
|
|
149
|
+
message: `remote MCP host '${host}' is not in the allowlist`,
|
|
150
|
+
path: `mcp.${name}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!policy.allowHooks &&
|
|
156
|
+
profile.hooks &&
|
|
157
|
+
Object.keys(profile.hooks).length > 0) {
|
|
158
|
+
issues.push({
|
|
159
|
+
level: "error",
|
|
160
|
+
code: "BLOCKED_HOOKS",
|
|
161
|
+
message: "hooks are not allowed (they run author-controlled shell commands automatically)",
|
|
162
|
+
path: "hooks",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (policy.allowConnections === false &&
|
|
166
|
+
profile.connections &&
|
|
167
|
+
profile.connections.length > 0) {
|
|
168
|
+
issues.push({
|
|
169
|
+
level: "error",
|
|
170
|
+
code: "BLOCKED_CONNECTIONS",
|
|
171
|
+
message: "hub connections are not allowed in an inline profile here — grant hub access through this surface's supported connection path instead",
|
|
172
|
+
path: "connections",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: !issues.some((i) => i.level === "error"),
|
|
177
|
+
issues,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable sandbox size vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* A size preset is a provider-neutral NAME for a compute tier (cpu / memory /
|
|
5
|
+
* disk). This module owns only the vocabulary — the names and their smallest→
|
|
6
|
+
* largest ordering — so the lowest shared layer can reference a size without
|
|
7
|
+
* depending on the sandbox SDK (`Capability.recommendedSize` lives here, and
|
|
8
|
+
* agent-interface must not depend on `@tangle-network/sandbox`).
|
|
9
|
+
*
|
|
10
|
+
* The concrete cpu/memory/disk numbers for each preset are the sandbox SDK's
|
|
11
|
+
* single source of truth (`@tangle-network/sandbox` → `SANDBOX_SIZE_PRESETS`),
|
|
12
|
+
* which imports these names. Mirrors how this package owns the `ReasoningEffort`
|
|
13
|
+
* vocabulary while backends own its native mapping.
|
|
14
|
+
*/
|
|
15
|
+
/** Compute tiers, ordered smallest → largest. */
|
|
16
|
+
export declare const SANDBOX_SIZE_PRESET_NAMES: readonly ["nano", "small", "medium", "large"];
|
|
17
|
+
/**
|
|
18
|
+
* A named compute tier for a sandbox. `nano` suits thin glue work (a single API
|
|
19
|
+
* call, a notify); `large` suits heavy builds over big repositories. Sizing is a
|
|
20
|
+
* per-task decision — a thin workflow step should not provision a maxed box.
|
|
21
|
+
*/
|
|
22
|
+
export type SandboxSizePreset = (typeof SANDBOX_SIZE_PRESET_NAMES)[number];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable sandbox size vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* A size preset is a provider-neutral NAME for a compute tier (cpu / memory /
|
|
5
|
+
* disk). This module owns only the vocabulary — the names and their smallest→
|
|
6
|
+
* largest ordering — so the lowest shared layer can reference a size without
|
|
7
|
+
* depending on the sandbox SDK (`Capability.recommendedSize` lives here, and
|
|
8
|
+
* agent-interface must not depend on `@tangle-network/sandbox`).
|
|
9
|
+
*
|
|
10
|
+
* The concrete cpu/memory/disk numbers for each preset are the sandbox SDK's
|
|
11
|
+
* single source of truth (`@tangle-network/sandbox` → `SANDBOX_SIZE_PRESETS`),
|
|
12
|
+
* which imports these names. Mirrors how this package owns the `ReasoningEffort`
|
|
13
|
+
* vocabulary while backends own its native mapping.
|
|
14
|
+
*/
|
|
15
|
+
/** Compute tiers, ordered smallest → largest. */
|
|
16
|
+
export const SANDBOX_SIZE_PRESET_NAMES = [
|
|
17
|
+
"nano",
|
|
18
|
+
"small",
|
|
19
|
+
"medium",
|
|
20
|
+
"large",
|
|
21
|
+
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-interface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
@@ -11,8 +12,19 @@
|
|
|
11
12
|
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/tangle-network/agent-sdk.git",
|
|
18
|
+
"directory": "packages/agent-interface"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org"
|
|
23
|
+
},
|
|
14
24
|
"files": [
|
|
15
|
-
"dist"
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
16
28
|
],
|
|
17
29
|
"dependencies": {
|
|
18
30
|
"zod": "4.4.3"
|
|
@@ -23,6 +35,7 @@
|
|
|
23
35
|
},
|
|
24
36
|
"scripts": {
|
|
25
37
|
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"check-types": "tsc --noEmit",
|
|
26
39
|
"clean": "rm -rf dist"
|
|
27
40
|
}
|
|
28
41
|
}
|