@vaia-lab/sdk 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +107 -151
- package/dist/cli.js +252 -0
- package/dist/index.cjs +698 -12
- package/dist/index.d.cts +754 -5
- package/dist/index.d.ts +754 -5
- package/dist/index.js +679 -11
- package/package.json +10 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VAIA Lab
|
|
4
|
+
|
|
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:
|
|
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
CHANGED
|
@@ -1,225 +1,181 @@
|
|
|
1
1
|
# @vaia-lab/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Agents with declared authority.** Connect your app to [Gandia-7](https://gandia7.com) and [Handeia](https://handeia.com), and let an assistant operate inside it — without writing the AI, and without giving it more power than you meant to.
|
|
4
4
|
|
|
5
|
-
Zero
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Install
|
|
5
|
+
Zero dependencies · Node 18+, Edge, Bun, Deno · TypeScript-first
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
|
-
npm
|
|
8
|
+
npm i @vaia-lab/sdk
|
|
9
|
+
npx vaia init
|
|
13
10
|
```
|
|
14
11
|
|
|
15
12
|
---
|
|
16
13
|
|
|
17
|
-
##
|
|
14
|
+
## The problem it solves
|
|
15
|
+
|
|
16
|
+
Most agent SDKs can say *"the agent may call this function."*
|
|
17
|
+
|
|
18
|
+
None of them can say *"it may pay up to $500 on its own, above that it asks, and it may never delete."*
|
|
19
|
+
|
|
20
|
+
That second sentence is the difference between a demo and something you let run on real data. Here it's mandatory: **a piece without declared authority does not compile.**
|
|
18
21
|
|
|
19
22
|
```ts
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return gandia.respond.surface(ctx.surface, {
|
|
33
|
-
card: () => ({ title: 'Alumnos', value: data.length }),
|
|
34
|
-
table: () => ({ columns: ['nombre', 'riesgo'], rows: data }),
|
|
35
|
-
text: () => `${data.length} alumnos en ${ctx.tenant.name}`,
|
|
36
|
-
})
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if (err instanceof VAIAError) return gandia.respond.error(err.message, err.status)
|
|
39
|
-
throw err
|
|
40
|
-
}
|
|
41
|
-
}
|
|
23
|
+
tools: [{
|
|
24
|
+
name: 'pay_invoice',
|
|
25
|
+
description: 'Pays a pending invoice.',
|
|
26
|
+
permission: 'write:payments',
|
|
27
|
+
authority: {
|
|
28
|
+
level: 'autonoma', // autonoma | requiere_aprobacion | prohibida
|
|
29
|
+
consequence: 'costosa', // reversible | costosa | irreversible
|
|
30
|
+
maxAmount: 500,
|
|
31
|
+
currency: 'MXN',
|
|
32
|
+
rationale: 'Small recurring invoices. Anything larger gets human review.',
|
|
33
|
+
},
|
|
34
|
+
}]
|
|
42
35
|
```
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
### Rules you cannot opt out of
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
| rule | why |
|
|
40
|
+
|---|---|
|
|
41
|
+
| **Irreversible is never autonomous** | Doesn't rely on the model behaving well — it relies on the config being **impossible to write** |
|
|
42
|
+
| Autonomous spending needs **a cap and a currency** | A bare `500` means nothing, and guessing the currency is how money disappears |
|
|
43
|
+
| No piece may **exceed its agent's ceiling** | Otherwise you declare a limited agent and slip it a tool that does what the agent can't |
|
|
44
|
+
| A tool **without a permission** is rejected | No permission means no one to ask for consent, and no one to revoke it from |
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
Validation happens **when you declare**, not in production. A bad contract breaks on your desk, not in front of your user.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
## The agent inside your app
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
- Returns `{ ctx: GandiaContext, raw: string }`
|
|
54
|
-
- Throws `VAIAError` (status 401) if invalid
|
|
52
|
+
You declare what you can do. The assistant reasons with that **plus what it knows about the user, which you never see**.
|
|
55
53
|
|
|
56
54
|
```ts
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
agent: {
|
|
56
|
+
actions: [
|
|
57
|
+
{ name: 'filter_results', description: 'Narrows the visible list.',
|
|
58
|
+
params: [{ name: 'city', type: 'string', description: 'City to filter by.' }] },
|
|
59
|
+
],
|
|
60
|
+
}
|
|
63
61
|
```
|
|
64
62
|
|
|
65
|
-
### `gandia.require(ctx, permission)`
|
|
66
|
-
|
|
67
|
-
Throws `VAIAError` (403) if the context doesn't have the required permission.
|
|
68
|
-
|
|
69
63
|
```ts
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
mountAgent({
|
|
65
|
+
capabilityId: 'com.my-app',
|
|
66
|
+
getContext: () => ({ route: location.pathname, claims: { visible: 12 } }),
|
|
67
|
+
onAction: async (name, args) => run(name, args),
|
|
68
|
+
})
|
|
73
69
|
```
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
**Why the agent doesn't live in your app:** if it brought its own AI, it wouldn't know the user, it would start from zero every session, and — most importantly — **it could never disagree with itself**. This way, if your app scores one option 90 and another 87, the assistant can still recommend the 87, because it knows something about the user your app has no business knowing.
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
**What it deliberately cannot do:** it may only request actions you declared. It doesn't improvise, doesn't touch the DOM, doesn't find workarounds. Anything that writes gets confirmed with the user first.
|
|
78
74
|
|
|
79
|
-
|
|
80
|
-
return gandia.respond.surface(ctx.surface, {
|
|
81
|
-
card: () => ({ title: 'Riesgo', value: 72, unit: '%', trend: 'up' }),
|
|
82
|
-
table: () => ({ columns: ['alumno', 'score'], rows }),
|
|
83
|
-
text: () => `El riesgo promedio es 72%`,
|
|
84
|
-
}, { audit: { data_sources: ['supabase:students'], records_accessed: 120 } })
|
|
85
|
-
```
|
|
75
|
+
---
|
|
86
76
|
|
|
87
|
-
|
|
77
|
+
## Borrowed connectors
|
|
88
78
|
|
|
89
|
-
|
|
79
|
+
Your app needs the user's GitHub or Drive. **You implement no OAuth, and you never receive their token.**
|
|
90
80
|
|
|
91
81
|
```ts
|
|
92
|
-
|
|
93
|
-
gandia.respond.table({ columns, rows, total })
|
|
94
|
-
gandia.respond.text(content, { markdown: true })
|
|
95
|
-
gandia.respond.widget({ url, height, width })
|
|
96
|
-
gandia.respond.action({ type, label, params, confirm })
|
|
97
|
-
gandia.respond.data(payload)
|
|
98
|
-
gandia.respond.ok()
|
|
99
|
-
gandia.respond.error(message, status, code?)
|
|
82
|
+
agent: { needs: ['github', 'drive'] }
|
|
100
83
|
```
|
|
101
84
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
### `gandia.jwt.verify(token, secret)`
|
|
85
|
+
You request an operation, the platform runs it with the token it already holds, and hands you back a trimmed result.
|
|
105
86
|
|
|
106
|
-
|
|
87
|
+
> If every app stored tokens, the attack surface would multiply by every developer who ships. One compromised app would hand over the GitHub and Drive of all its users. Borrowed, it only reaches what the user granted — rate-limited, audited, and revocable instantly.
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
// In your iframe entry route:
|
|
110
|
-
const claims = await gandia.jwt.fromUrl(request.url, process.env.GANDIA_KEY_SECRET!)
|
|
111
|
-
// claims.sub → user_id
|
|
112
|
-
// claims.tenant_id → institution id
|
|
113
|
-
// claims.email → user email (if available)
|
|
114
|
-
// claims.permissions → what the user can do
|
|
115
|
-
```
|
|
89
|
+
Read-only. Writing to an external service is never lent: that's what the service itself is for.
|
|
116
90
|
|
|
117
91
|
---
|
|
118
92
|
|
|
119
|
-
|
|
93
|
+
## MCP, wrapped
|
|
120
94
|
|
|
121
|
-
|
|
95
|
+
MCP brings the catalog and the transport. This SDK brings the authority layer **MCP cannot express**.
|
|
122
96
|
|
|
123
97
|
```ts
|
|
124
|
-
const {
|
|
125
|
-
// ctx.user.id, ctx.user.email
|
|
126
|
-
// ctx.permissions, ctx.surface, ctx.query
|
|
98
|
+
const { tool, warnings } = fromMCPTool(mcpTool, authority, 'read:web')
|
|
127
99
|
```
|
|
128
100
|
|
|
101
|
+
**Importing something from the internet grants it nothing.** Authority is assigned separately, always. The server's own hints only ever produce warnings — *"it flags this as destructive but you declared it reversible"* — never decisions.
|
|
102
|
+
|
|
103
|
+
Outbound, `toMCPTool()` **withholds anything requiring approval**: exposing it would offer an external agent something even you can't run unattended.
|
|
104
|
+
|
|
129
105
|
---
|
|
130
106
|
|
|
131
|
-
|
|
107
|
+
## Use it without our platform
|
|
132
108
|
|
|
133
|
-
|
|
109
|
+
**Ten of the thirteen exports need no account, no network and no keys.** The authority layer is not tied to Gandia-7 or Handeia — it works with any LLM, any framework, any stack.
|
|
134
110
|
|
|
135
111
|
```ts
|
|
136
|
-
|
|
137
|
-
import { defineCapability } from '@vaia-lab/sdk'
|
|
138
|
-
|
|
139
|
-
export default defineCapability({
|
|
140
|
-
id: 'mx.monitor-riesgo-academico', // reverse-domain
|
|
141
|
-
name: 'Monitor de Riesgo Académico',
|
|
142
|
-
version: '1.0.0',
|
|
143
|
-
target: 'gandia', // 'gandia' | 'handeia' | 'both'
|
|
144
|
-
type: 'app', // 'app' | 'ia' | 'skill' | 'eco'
|
|
145
|
-
level: 'artefacto', // 'widget' | 'artefacto' | 'espacio'
|
|
146
|
-
sector: 'educacion',
|
|
147
|
-
surfaces: {
|
|
148
|
-
card: { endpoint: '/api/gandia/invoke' },
|
|
149
|
-
table: { endpoint: '/api/gandia/invoke' },
|
|
150
|
-
text: { endpoint: '/api/gandia/invoke' },
|
|
151
|
-
},
|
|
152
|
-
permissions: ['read:students', 'read:grades', 'write:alerts'],
|
|
153
|
-
risk: 'medium',
|
|
154
|
-
})
|
|
155
|
-
```
|
|
112
|
+
import { validatePieces, checkAuthority, fromMCPTool } from '@vaia-lab/sdk'
|
|
156
113
|
|
|
157
|
-
|
|
114
|
+
// Validate your agent's tools at build time — with your own runtime
|
|
115
|
+
const errors = validatePieces({ tools: myTools })
|
|
116
|
+
if (errors.length) throw new Error(errors.join('\n'))
|
|
158
117
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# Validate existing manifest:
|
|
164
|
-
npx vaia-sdk manifest --validate
|
|
118
|
+
// Gate a call before it happens, wherever your agent runs
|
|
119
|
+
const ok = checkAuthority(tool.authority, { amount: 900, currency: 'MXN' })
|
|
120
|
+
if (!ok.ok) return askHuman(ok.reason)
|
|
165
121
|
```
|
|
166
122
|
|
|
123
|
+
| works standalone | needs the platform |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `defineCapability` · `toManifest` | `gandia.verify` · `handeia.jwt` |
|
|
126
|
+
| `validatePieces` · `checkAuthority` · `requiresApproval` | `mountAgent` |
|
|
127
|
+
| `validateAgentSurface` · `validateActionCall` | |
|
|
128
|
+
| `fromMCPTool` · `toMCPTool` · `toMCPTools` | |
|
|
129
|
+
|
|
130
|
+
Bring your own model. Bring your own orchestrator. Keep the guardrails.
|
|
131
|
+
|
|
167
132
|
---
|
|
168
133
|
|
|
169
|
-
##
|
|
134
|
+
## Identity, without a second login
|
|
170
135
|
|
|
171
|
-
|
|
136
|
+
Your app opens from Gandia-7 or Handeia with the user's session already resolved.
|
|
172
137
|
|
|
173
138
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const { ctx } = await gandia.verify(req, secret)
|
|
178
|
-
// ...
|
|
179
|
-
} catch (err) {
|
|
180
|
-
if (err instanceof VAIAError) {
|
|
181
|
-
// err.message → human-readable message (Spanish)
|
|
182
|
-
// err.code → machine-readable: 'HMAC_INVALID', 'JWT_EXPIRED', 'PERMISSION_DENIED', etc.
|
|
183
|
-
// err.status → HTTP status: 401, 403, 400, 422
|
|
184
|
-
return gandia.respond.error(err.message, err.status, err.code)
|
|
185
|
-
}
|
|
186
|
-
throw err
|
|
187
|
-
}
|
|
139
|
+
const claims = await handeia.jwt.fromUrl(request.url, process.env.HANDEIA_KEY_SECRET!)
|
|
188
140
|
```
|
|
189
141
|
|
|
190
|
-
|
|
142
|
+
And to verify a call genuinely came from the platform:
|
|
191
143
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
144
|
+
```ts
|
|
145
|
+
const { ctx } = await gandia.verify(request, process.env.GANDIA_KEY_SECRET!)
|
|
146
|
+
gandia.require(ctx, 'read:students')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Constant-time HMAC comparison, ±5 min window, and the health probe **requires a signature too**.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## The 7 pieces
|
|
154
|
+
|
|
155
|
+
`skills` · `tools` · `workflows` · `agents` · `personalities` · `modalities` · `permissions`
|
|
156
|
+
|
|
157
|
+
All declared, all validated, all carried in the manifest — so the portal can show **what authority a capability asks for before anyone installs it**.
|
|
203
158
|
|
|
204
159
|
---
|
|
205
160
|
|
|
206
161
|
## CLI
|
|
207
162
|
|
|
208
163
|
```bash
|
|
209
|
-
npx vaia
|
|
210
|
-
npx vaia
|
|
211
|
-
npx vaia
|
|
212
|
-
npx vaia-sdk version # SDK version
|
|
164
|
+
npx vaia init # drops a ready vaia.config.ts — no account, no keys, no network
|
|
165
|
+
npx vaia manifest # generates the manifest from your config
|
|
166
|
+
npx vaia sign # signs a payload for testing
|
|
213
167
|
```
|
|
214
168
|
|
|
215
169
|
---
|
|
216
170
|
|
|
217
|
-
##
|
|
171
|
+
## Security
|
|
218
172
|
|
|
219
|
-
|
|
173
|
+
See [SECURITY.md](./SECURITY.md) — what the SDK guarantees, and what's on you, such as deduplicating `call_id` if replay within the window matters to you.
|
|
220
174
|
|
|
221
|
-
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). What's open is **how to declare responsible agents**; the engines that run them are private.
|
|
222
178
|
|
|
223
179
|
## License
|
|
224
180
|
|
|
225
|
-
MIT
|
|
181
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
5
5
|
import { resolve, join } from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
6
7
|
import { createInterface } from "readline";
|
|
7
8
|
|
|
8
9
|
// src/crypto.ts
|
|
@@ -22,6 +23,107 @@ async function hmacSign(secret, data) {
|
|
|
22
23
|
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// src/agent.ts
|
|
27
|
+
var AGENT_PROTOCOL_VERSION = 1;
|
|
28
|
+
var NOMBRE_ACCION = /^[a-z][a-z0-9_]{1,48}$/;
|
|
29
|
+
function validateAgentSurface(cfg) {
|
|
30
|
+
const errores = [];
|
|
31
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const accion of cfg.actions ?? []) {
|
|
33
|
+
if (!NOMBRE_ACCION.test(accion.name)) {
|
|
34
|
+
errores.push(`Acci\xF3n "${accion.name}": el nombre debe ser min\xFAsculas, n\xFAmeros o guion bajo.`);
|
|
35
|
+
}
|
|
36
|
+
if (vistos.has(accion.name)) {
|
|
37
|
+
errores.push(`Acci\xF3n "${accion.name}": declarada dos veces.`);
|
|
38
|
+
}
|
|
39
|
+
vistos.add(accion.name);
|
|
40
|
+
if (!accion.description?.trim()) {
|
|
41
|
+
errores.push(`Acci\xF3n "${accion.name}": falta la descripci\xF3n, que es lo que el agente lee para elegirla.`);
|
|
42
|
+
}
|
|
43
|
+
if (accion.writes && !accion.permission) {
|
|
44
|
+
errores.push(`Acci\xF3n "${accion.name}": modifica datos, as\xED que necesita un permiso declarado.`);
|
|
45
|
+
}
|
|
46
|
+
for (const p of accion.params ?? []) {
|
|
47
|
+
if (!p.name?.trim()) errores.push(`Acci\xF3n "${accion.name}": un par\xE1metro no tiene nombre.`);
|
|
48
|
+
if (!p.description?.trim()) {
|
|
49
|
+
errores.push(`Acci\xF3n "${accion.name}", par\xE1metro "${p.name}": falta la descripci\xF3n.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (cfg.queryEndpoint && !cfg.queryEndpoint.startsWith("/")) {
|
|
54
|
+
errores.push('queryEndpoint debe ser una ruta de tu propio servidor, empezando por "/".');
|
|
55
|
+
}
|
|
56
|
+
return errores;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/pieces.ts
|
|
60
|
+
var NOMBRE = /^[a-z][a-z0-9_]{1,48}$/;
|
|
61
|
+
var ORDEN = {
|
|
62
|
+
prohibida: 0,
|
|
63
|
+
requiere_aprobacion: 1,
|
|
64
|
+
autonoma: 2
|
|
65
|
+
};
|
|
66
|
+
function validatePieces(cfg) {
|
|
67
|
+
const errores = [];
|
|
68
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
69
|
+
const revisarNombre = (pieza, name) => {
|
|
70
|
+
if (!NOMBRE.test(name)) errores.push(`${pieza} "${name}": el nombre debe ser min\xFAsculas, n\xFAmeros o guion bajo.`);
|
|
71
|
+
if (vistos.has(name)) errores.push(`"${name}": hay dos piezas con el mismo nombre.`);
|
|
72
|
+
vistos.add(name);
|
|
73
|
+
};
|
|
74
|
+
const revisarAutoridad = (pieza, a) => {
|
|
75
|
+
if (a.consequence === "irreversible" && a.level === "autonoma") {
|
|
76
|
+
errores.push(`${pieza}: una acci\xF3n irreversible no puede ser aut\xF3noma \u2014 como m\xEDnimo requiere aprobaci\xF3n.`);
|
|
77
|
+
}
|
|
78
|
+
if (a.maxAmount !== void 0) {
|
|
79
|
+
if (a.maxAmount <= 0) errores.push(`${pieza}: el tope de gasto debe ser mayor que cero.`);
|
|
80
|
+
if (!a.currency) errores.push(`${pieza}: hay tope de gasto pero no se declar\xF3 la moneda.`);
|
|
81
|
+
}
|
|
82
|
+
if (a.consequence === "costosa" && a.level === "autonoma" && a.maxAmount === void 0) {
|
|
83
|
+
errores.push(`${pieza}: es aut\xF3noma y cuesta dinero, as\xED que necesita un tope declarado.`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
for (const s of cfg.skills ?? []) {
|
|
87
|
+
revisarNombre("Skill", s.name);
|
|
88
|
+
if (!s.description?.trim()) errores.push(`Skill "${s.name}": falta la descripci\xF3n, que es lo que el modelo lee para elegirla.`);
|
|
89
|
+
if (s.evidence?.required && (s.evidence.accepts?.length ?? 0) === 0) {
|
|
90
|
+
errores.push(`Skill "${s.name}": exige evidencia pero no declara qu\xE9 tipos acepta.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const t of cfg.tools ?? []) {
|
|
94
|
+
revisarNombre("Herramienta", t.name);
|
|
95
|
+
if (!t.description?.trim()) errores.push(`Herramienta "${t.name}": falta la descripci\xF3n.`);
|
|
96
|
+
if (!t.permission?.trim()) errores.push(`Herramienta "${t.name}": toca el mundo real, as\xED que necesita un permiso declarado.`);
|
|
97
|
+
revisarAutoridad(`Herramienta "${t.name}"`, t.authority);
|
|
98
|
+
}
|
|
99
|
+
for (const w of cfg.workflows ?? []) {
|
|
100
|
+
revisarNombre("Workflow", w.name);
|
|
101
|
+
if ((w.steps?.length ?? 0) === 0) errores.push(`Workflow "${w.name}": no tiene pasos.`);
|
|
102
|
+
revisarAutoridad(`Workflow "${w.name}"`, w.authority);
|
|
103
|
+
}
|
|
104
|
+
const porNombre = new Map((cfg.tools ?? []).map((t) => [t.name, t]));
|
|
105
|
+
for (const a of cfg.agents ?? []) {
|
|
106
|
+
revisarNombre("Agente", a.name);
|
|
107
|
+
if (!a.purpose?.trim()) {
|
|
108
|
+
errores.push(`Agente "${a.name}": falta el prop\xF3sito \u2014 para qu\xE9 existe.`);
|
|
109
|
+
}
|
|
110
|
+
revisarAutoridad(`Agente "${a.name}"`, a.authority);
|
|
111
|
+
for (const nombreTool of a.tools ?? []) {
|
|
112
|
+
const tool = porNombre.get(nombreTool);
|
|
113
|
+
if (!tool) {
|
|
114
|
+
errores.push(`Agente "${a.name}": usa la herramienta "${nombreTool}", que no est\xE1 declarada.`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ORDEN[tool.authority.level] > ORDEN[a.authority.level]) {
|
|
118
|
+
errores.push(
|
|
119
|
+
`Agente "${a.name}": su herramienta "${nombreTool}" tiene m\xE1s autoridad que \xE9l. Ninguna pieza puede superar el techo de su agente.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return errores;
|
|
125
|
+
}
|
|
126
|
+
|
|
25
127
|
// src/define.ts
|
|
26
128
|
function defineCapability(config) {
|
|
27
129
|
const required = ["id", "name", "version", "target", "type", "sector", "permissions", "risk"];
|
|
@@ -38,6 +140,20 @@ function defineCapability(config) {
|
|
|
38
140
|
if (Object.keys(config.surfaces).length === 0) {
|
|
39
141
|
throw new Error(`[@vaia/sdk] defineCapability: 'surfaces' no puede estar vac\xEDo. Define al menos un surface con su endpoint.`);
|
|
40
142
|
}
|
|
143
|
+
if (config.pieces) {
|
|
144
|
+
const errores = validatePieces(config.pieces);
|
|
145
|
+
if (errores.length > 0) {
|
|
146
|
+
throw new Error(`[@vaia/sdk] defineCapability: piezas inv\xE1lidas:
|
|
147
|
+
- ${errores.join("\n - ")}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (config.agent) {
|
|
151
|
+
const errores = validateAgentSurface(config.agent);
|
|
152
|
+
if (errores.length > 0) {
|
|
153
|
+
throw new Error(`[@vaia/sdk] defineCapability: superficie de agente inv\xE1lida:
|
|
154
|
+
- ${errores.join("\n - ")}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
41
157
|
return config;
|
|
42
158
|
}
|
|
43
159
|
function toManifest(config) {
|
|
@@ -52,6 +168,16 @@ function toManifest(config) {
|
|
|
52
168
|
level: config.level,
|
|
53
169
|
sector: config.sector,
|
|
54
170
|
surfaces,
|
|
171
|
+
// El agente viaja en el manifest para que el portal y Handeia sepan qué
|
|
172
|
+
// puede hacer este espacio sin abrir su código.
|
|
173
|
+
agent: config.agent ? {
|
|
174
|
+
protocol: AGENT_PROTOCOL_VERSION,
|
|
175
|
+
actions: config.agent.actions ?? [],
|
|
176
|
+
query_endpoint: config.agent.queryEndpoint
|
|
177
|
+
} : void 0,
|
|
178
|
+
// Las piezas viajan al manifest: el portal necesita mostrar qué autoridad
|
|
179
|
+
// pide una capacidad ANTES de que alguien la instale.
|
|
180
|
+
pieces: config.pieces,
|
|
55
181
|
permissions: config.permissions,
|
|
56
182
|
risk: config.risk,
|
|
57
183
|
has_own_auth: config.has_own_auth ?? false,
|
|
@@ -72,6 +198,10 @@ var args = process.argv.slice(2);
|
|
|
72
198
|
var cmd = args[0];
|
|
73
199
|
async function main() {
|
|
74
200
|
switch (cmd) {
|
|
201
|
+
case "init":
|
|
202
|
+
return cmdInit();
|
|
203
|
+
case "doctor":
|
|
204
|
+
return cmdDoctor();
|
|
75
205
|
case "manifest":
|
|
76
206
|
return cmdManifest();
|
|
77
207
|
case "sign":
|
|
@@ -85,6 +215,126 @@ async function main() {
|
|
|
85
215
|
printHelp();
|
|
86
216
|
}
|
|
87
217
|
}
|
|
218
|
+
async function cmdInit() {
|
|
219
|
+
const cwd = process.cwd();
|
|
220
|
+
const out = join(cwd, "vaia.config.ts");
|
|
221
|
+
if (existsSync(out)) {
|
|
222
|
+
console.error("\u2717 Ya existe vaia.config.ts en esta carpeta.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const plantilla = `import { defineCapability } from '@vaia-lab/sdk'
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Tu capacidad, declarada.
|
|
229
|
+
*
|
|
230
|
+
* Esto es la \xFAnica fuente de verdad: el portal la lee y arma el manifest solo.
|
|
231
|
+
*/
|
|
232
|
+
export default defineCapability({
|
|
233
|
+
id: 'mx.mi-capacidad',
|
|
234
|
+
name: 'Mi capacidad',
|
|
235
|
+
version: '0.1.0',
|
|
236
|
+
target: 'handeia', // 'gandia' | 'handeia' | 'both'
|
|
237
|
+
type: 'app',
|
|
238
|
+
sector: 'general',
|
|
239
|
+
description: 'Describe en una l\xEDnea qu\xE9 resuelve.',
|
|
240
|
+
|
|
241
|
+
surfaces: {
|
|
242
|
+
text: { endpoint: '/api/vaia/invoke' },
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// \u2500\u2500 Las piezas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
246
|
+
// La autoridad NO es opcional: hay que decir hasta d\xF3nde puede llegar cada
|
|
247
|
+
// cosa. Lo irreversible nunca puede ser aut\xF3nomo, y lo que gasta dinero
|
|
248
|
+
// necesita tope y moneda. Si te lo saltas, esto no compila.
|
|
249
|
+
pieces: {
|
|
250
|
+
tools: [
|
|
251
|
+
{
|
|
252
|
+
name: 'consultar_datos',
|
|
253
|
+
description: 'Lee datos del usuario para responder preguntas.',
|
|
254
|
+
permission: 'read:datos',
|
|
255
|
+
authority: {
|
|
256
|
+
level: 'autonoma', // 'autonoma' | 'requiere_aprobacion' | 'prohibida'
|
|
257
|
+
consequence: 'reversible', // 'reversible' | 'costosa' | 'irreversible'
|
|
258
|
+
rationale: 'Solo lee. No cambia nada del usuario.',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// \u2500\u2500 El agente dentro de tu app \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
265
|
+
// El asistente de Handeia vive en tu superficie. T\xFA declaras qu\xE9 sabes
|
|
266
|
+
// hacer; \xE9l razona con eso m\xE1s lo que sabe del usuario, que t\xFA nunca ves.
|
|
267
|
+
agent: {
|
|
268
|
+
greeting: '\xBFEn qu\xE9 te ayudo?',
|
|
269
|
+
actions: [
|
|
270
|
+
{
|
|
271
|
+
name: 'ir_a',
|
|
272
|
+
description: 'Lleva al usuario a una secci\xF3n de la app.',
|
|
273
|
+
params: [
|
|
274
|
+
{ name: 'seccion', type: 'string', description: 'Secci\xF3n destino.', required: true },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
// Servicios que necesitas consultar. NUNCA recibes el token del usuario:
|
|
279
|
+
// pides la operaci\xF3n y la plataforma te devuelve solo el resultado.
|
|
280
|
+
needs: [],
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
permissions: ['read:datos'],
|
|
284
|
+
risk: 'low',
|
|
285
|
+
})
|
|
286
|
+
`;
|
|
287
|
+
writeFileSync(out, plantilla, "utf8");
|
|
288
|
+
console.log("\u2713 vaia.config.ts creado");
|
|
289
|
+
console.log("");
|
|
290
|
+
console.log(" Siguiente:");
|
|
291
|
+
console.log(" 1. Edita el id, el nombre y lo que sabe hacer tu app.");
|
|
292
|
+
console.log(" 2. npx vaia manifest \u2192 genera el manifest");
|
|
293
|
+
console.log(" 3. S\xFAbelo desde el portal de developers.");
|
|
294
|
+
}
|
|
295
|
+
async function cmdDoctor() {
|
|
296
|
+
const cwd = process.cwd();
|
|
297
|
+
const configPath = resolve(cwd, "vaia.config.js");
|
|
298
|
+
if (!existsSync(configPath)) {
|
|
299
|
+
console.error("\u2717 No encontr\xE9 vaia.config.js. Compila tu vaia.config.ts primero, o corre `vaia init`.");
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
let config;
|
|
303
|
+
try {
|
|
304
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
305
|
+
config = mod.default ?? mod.config;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(`\u2717 No pude cargar vaia.config.js:
|
|
308
|
+
${err instanceof Error ? err.message : String(err)}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const avisos = [];
|
|
312
|
+
const graves = [];
|
|
313
|
+
for (const t of config.pieces?.tools ?? []) {
|
|
314
|
+
if (t.authority.level === "autonoma" && t.authority.consequence === "costosa" && !t.authority.rationale) {
|
|
315
|
+
avisos.push(`"${t.name}" gasta dinero sola y no explica por qu\xE9 se le dio esa confianza.`);
|
|
316
|
+
}
|
|
317
|
+
if (t.permission && !config.permissions.includes(t.permission)) {
|
|
318
|
+
graves.push(`"${t.name}" pide el permiso "${t.permission}", que no est\xE1 en la lista de permisos de la capacidad.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const usados = new Set((config.pieces?.tools ?? []).map((t) => t.permission));
|
|
322
|
+
for (const p of config.permissions) {
|
|
323
|
+
if (!usados.has(p)) avisos.push(`El permiso "${p}" se pide pero ninguna herramienta lo usa. Pedir de m\xE1s incomoda al usuario.`);
|
|
324
|
+
}
|
|
325
|
+
for (const a of config.agent?.actions ?? []) {
|
|
326
|
+
if (a.writes && !a.permission) {
|
|
327
|
+
graves.push(`La acci\xF3n "${a.name}" modifica datos y no declara permiso.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (graves.length === 0 && avisos.length === 0) {
|
|
331
|
+
console.log("\u2713 Todo en orden.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const g of graves) console.error(`\u2717 ${g}`);
|
|
335
|
+
for (const a of avisos) console.log(`\u26A0 ${a}`);
|
|
336
|
+
if (graves.length > 0) process.exit(1);
|
|
337
|
+
}
|
|
88
338
|
async function cmdManifest() {
|
|
89
339
|
const cwd = process.cwd();
|
|
90
340
|
const configPath = resolve(cwd, "vaia.config.js");
|
|
@@ -182,6 +432,8 @@ function printHelp() {
|
|
|
182
432
|
`@vaia/sdk v${pkg.version}`,
|
|
183
433
|
"",
|
|
184
434
|
"Comandos:",
|
|
435
|
+
" vaia-sdk init Crea vaia.config.ts listo para editar",
|
|
436
|
+
" vaia-sdk doctor Revisa tu configuraci\xF3n y avisa qu\xE9 est\xE1 mal",
|
|
185
437
|
" vaia-sdk manifest Genera gandia.manifest.json desde vaia.config.js",
|
|
186
438
|
" vaia-sdk manifest --validate Valida un gandia.manifest.json existente",
|
|
187
439
|
" vaia-sdk sign [payload] Firma un payload con GANDIA_KEY_SECRET",
|