@sentropic/artifacts 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +95 -2
- package/dist/adapters/postgres.d.ts +35 -0
- package/dist/adapters/postgres.js +301 -0
- package/dist/adapters/s3.d.ts +19 -0
- package/dist/adapters/s3.js +70 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +44 -0
- package/dist/codecs.d.ts +12 -0
- package/dist/codecs.js +77 -0
- package/dist/content/api-profile.d.ts +38 -0
- package/dist/content/api-profile.js +40 -0
- package/dist/content/grants.d.ts +57 -0
- package/dist/content/grants.js +143 -0
- package/dist/content/handler.d.ts +34 -0
- package/dist/content/handler.js +129 -0
- package/dist/content/headers.d.ts +25 -0
- package/dist/content/headers.js +56 -0
- package/dist/content/routing.d.ts +13 -0
- package/dist/content/routing.js +59 -0
- package/dist/contracts.d.ts +102 -0
- package/dist/contracts.js +25 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +502 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +29 -0
- package/dist/mcp.d.ts +68 -0
- package/dist/mcp.js +293 -0
- package/dist/parameters.d.ts +29 -0
- package/dist/parameters.js +65 -0
- package/dist/provider-definition.d.ts +36 -0
- package/dist/provider-definition.js +45 -0
- package/dist/server/access.d.ts +39 -0
- package/dist/server/access.js +53 -0
- package/dist/server/authorization.d.ts +17 -0
- package/dist/server/authorization.js +12 -0
- package/dist/server/bindings.d.ts +34 -0
- package/dist/server/bindings.js +60 -0
- package/dist/server/fork.d.ts +51 -0
- package/dist/server/fork.js +75 -0
- package/dist/server/gc.d.ts +24 -0
- package/dist/server/gc.js +90 -0
- package/dist/server/idempotency.d.ts +23 -0
- package/dist/server/idempotency.js +38 -0
- package/dist/server/lifecycle.d.ts +57 -0
- package/dist/server/lifecycle.js +184 -0
- package/dist/server/object-intents.d.ts +41 -0
- package/dist/server/object-intents.js +62 -0
- package/dist/server/ports.d.ts +34 -0
- package/dist/server/ports.js +50 -0
- package/dist/server/publication.d.ts +67 -0
- package/dist/server/publication.js +154 -0
- package/dist/server/reader-profile.d.ts +27 -0
- package/dist/server/reader-profile.js +18 -0
- package/dist/server/runtime.d.ts +33 -0
- package/dist/server/runtime.js +44 -0
- package/dist/server/service.d.ts +19 -0
- package/dist/server/service.js +32 -0
- package/dist/server/sources.d.ts +27 -0
- package/dist/server/sources.js +56 -0
- package/dist/server/templates.d.ts +33 -0
- package/dist/server/templates.js +86 -0
- package/dist/server/uploads.d.ts +29 -0
- package/dist/server/uploads.js +86 -0
- package/package.json +162 -4
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { computeManifestDigest } from '@sentropic/mcp-platform/tool-configuration';
|
|
2
|
+
export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
|
|
3
|
+
export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
|
|
4
|
+
export const ARTIFACTS_PROVIDER_VERSION = '0.1.0';
|
|
5
|
+
export const ARTIFACTS_TOOL_NAMES = [
|
|
6
|
+
'artifacts_get_template', 'artifacts_get_api_guide', 'artifacts_ensure',
|
|
7
|
+
'artifacts_publish', 'artifacts_create_upload', 'artifacts_commit_upload',
|
|
8
|
+
'artifacts_list', 'artifacts_get', 'artifacts_get_source', 'artifacts_fork',
|
|
9
|
+
'artifacts_set_access', 'artifacts_promote', 'artifacts_renew',
|
|
10
|
+
'artifacts_delete', 'artifacts_restore',
|
|
11
|
+
];
|
|
12
|
+
const str = { type: 'string', minLength: 1 };
|
|
13
|
+
const int = (minimum, maximum) => ({
|
|
14
|
+
type: 'integer', minimum, ...(maximum === undefined ? {} : { maximum }),
|
|
15
|
+
});
|
|
16
|
+
const obj = (properties, required = []) => ({
|
|
17
|
+
type: 'object', properties, required, additionalProperties: false,
|
|
18
|
+
});
|
|
19
|
+
const record = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: { type: ['string', 'number', 'boolean', 'null'] },
|
|
22
|
+
};
|
|
23
|
+
const fileBytes = { type: 'array', items: int(0, 255) };
|
|
24
|
+
const fileEntry = obj({ path: str, mimeType: str, bytes: fileBytes }, ['path', 'mimeType', 'bytes']);
|
|
25
|
+
const sourceRef = obj({
|
|
26
|
+
kind: { type: 'string', enum: ['inline-original', 'base-commit', 'recipe'] },
|
|
27
|
+
commit: { type: 'string' },
|
|
28
|
+
steps: { type: 'array', items: { type: 'string' } },
|
|
29
|
+
}, ['kind']);
|
|
30
|
+
const idOnly = obj({ artifactId: str }, ['artifactId']);
|
|
31
|
+
export const ARTIFACTS_TOOL_SPECS = [
|
|
32
|
+
{ name: 'artifacts_get_template', description: 'Get an approved template kit (sources + build recipe).', inputSchema: obj({ templateId: str }, ['templateId']), mutability: 'read-only' },
|
|
33
|
+
{ name: 'artifacts_get_api_guide', description: 'Get the versioned artifacts API guide.', inputSchema: obj({ version: str }), mutability: 'read-only' },
|
|
34
|
+
{ name: 'artifacts_ensure', description: 'Resolve or create the private artifact view for a binding.', inputSchema: obj({ bindingName: str, storageScopeId: str, idempotencyKey: str, paramsHash: str, parameters: record }, ['bindingName', 'idempotencyKey']), mutability: 'state-transition', idempotent: true },
|
|
35
|
+
{ name: 'artifacts_publish', description: 'Publish inline files as a new release.', inputSchema: obj({ artifactId: str, files: { type: 'array', minItems: 1, items: fileEntry }, profile: record, source: sourceRef, idempotencyKey: str, paramsHash: str }, ['artifactId', 'files']), mutability: 'append', idempotent: true },
|
|
36
|
+
{ name: 'artifacts_create_upload', description: 'Open a bounded upload session for large bytes.', inputSchema: obj({ artifactId: str, mimeType: str, maxSize: int(1) }, ['artifactId', 'mimeType']), mutability: 'state-transition' },
|
|
37
|
+
{ name: 'artifacts_commit_upload', description: 'Verify staged bytes and publish them as a release.', inputSchema: obj({ artifactId: str, uploadId: str, path: str, mimeType: str, digest: str, size: int(0), profile: record, idempotencyKey: str, paramsHash: str }, ['artifactId', 'uploadId', 'path', 'mimeType', 'digest', 'size']), mutability: 'append', idempotent: true },
|
|
38
|
+
{ name: 'artifacts_list', description: 'List artifacts visible to the caller (ACL before pagination).', inputSchema: obj({ limit: int(1, 100), offset: int(0) }), mutability: 'read-only' },
|
|
39
|
+
{ name: 'artifacts_get', description: 'Get artifact metadata (grants visible to the owner only).', inputSchema: idOnly, mutability: 'read-only' },
|
|
40
|
+
{ name: 'artifacts_get_source', description: 'Export the attached source reference (separate right).', inputSchema: idOnly, mutability: 'read-only' },
|
|
41
|
+
{ name: 'artifacts_fork', description: 'Fork privately under a fresh identity (no ACL copied).', inputSchema: idOnly, mutability: 'state-transition' },
|
|
42
|
+
{ name: 'artifacts_set_access', description: 'Grant or revoke read/source access, or toggle public mode.', inputSchema: obj({ artifactId: str, action: { type: 'string', enum: ['grant', 'revoke', 'set-public'] }, principalId: str, grant: { type: 'string', enum: ['read', 'source'] }, isPublic: { type: 'boolean' } }, ['artifactId', 'action']), mutability: 'patch', confirm: true },
|
|
43
|
+
{ name: 'artifacts_promote', description: 'Select a release explicitly (expected-current CAS).', inputSchema: obj({ artifactId: str, releaseId: str, expectedCurrent: str }, ['artifactId', 'releaseId', 'expectedCurrent']), mutability: 'patch' },
|
|
44
|
+
{ name: 'artifacts_renew', description: 'Renew the artifact retention timestamp.', inputSchema: idOnly, mutability: 'patch' },
|
|
45
|
+
{ name: 'artifacts_delete', description: 'Withdraw into pending deletion (undo via artifacts_restore).', inputSchema: idOnly, mutability: 'delete', confirm: true },
|
|
46
|
+
{ name: 'artifacts_restore', description: 'Cancel a pending deletion or withdrawal before purge.', inputSchema: idOnly, mutability: 'state-transition' },
|
|
47
|
+
];
|
|
48
|
+
/** Kernel provider parameter schema (typed non-secret host settings). */
|
|
49
|
+
export const ARTIFACTS_PARAMETER_SCHEMA = {
|
|
50
|
+
properties: {
|
|
51
|
+
defaultStorageScopeId: { type: 'string', minLength: 1, description: 'Fallback storage scope for artifacts_ensure.' },
|
|
52
|
+
uploadBudgetBytes: { type: 'integer', minimum: 1, description: 'Declared bounded-upload budget.' },
|
|
53
|
+
},
|
|
54
|
+
required: [],
|
|
55
|
+
};
|
|
56
|
+
/** Build the sentropic.artifacts manifest (host scope bound pre-freeze). */
|
|
57
|
+
export function createArtifactsManifest(options = {}) {
|
|
58
|
+
const scopes = options.invocationScope === undefined ? [] : [options.invocationScope];
|
|
59
|
+
const tools = ARTIFACTS_TOOL_SPECS.map((spec) => ({
|
|
60
|
+
kind: 'tool',
|
|
61
|
+
name: spec.name,
|
|
62
|
+
description: spec.description,
|
|
63
|
+
requiredScopes: [...scopes],
|
|
64
|
+
requiredClaims: [],
|
|
65
|
+
inputSchema: spec.inputSchema,
|
|
66
|
+
outputSchema: { type: 'object' },
|
|
67
|
+
redactionClass: 'low',
|
|
68
|
+
mutability: spec.mutability,
|
|
69
|
+
mutatesExternalSystem: spec.mutability !== 'read-only',
|
|
70
|
+
idempotency: spec.idempotent === true
|
|
71
|
+
? { required: true, scope: 'principal' }
|
|
72
|
+
: { required: false },
|
|
73
|
+
gates: {
|
|
74
|
+
requiresElicitation: false,
|
|
75
|
+
requiresHumanConfirmation: spec.confirm === true,
|
|
76
|
+
requiresPrincipalGate: false,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
appId: ARTIFACTS_APP_ID,
|
|
81
|
+
providerId: ARTIFACTS_PROVIDER_ID,
|
|
82
|
+
version: ARTIFACTS_PROVIDER_VERSION,
|
|
83
|
+
displayName: options.displayName ?? 'Sentropic Artifacts',
|
|
84
|
+
resources: [],
|
|
85
|
+
tools,
|
|
86
|
+
prompts: [],
|
|
87
|
+
authz: { requiredClaims: [], scopes: [...scopes], tenantResolution: 'custom-resolver' },
|
|
88
|
+
audit: { eventKinds: ['invoke'] },
|
|
89
|
+
durability: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function needString(args, name) {
|
|
93
|
+
const value = args[name];
|
|
94
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
95
|
+
throw new Error(`invalid request: missing ${name}`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function optString(args, name) {
|
|
100
|
+
const value = args[name];
|
|
101
|
+
return typeof value === 'string' && value.length !== 0 ? value : undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Create the 15-tool executor over any conforming artifacts HTTP app. */
|
|
104
|
+
export function createArtifactsToolExecutor(deps) {
|
|
105
|
+
const call = (actorId, method, path, body) => deps.app.handle({ method, path, actorId, body });
|
|
106
|
+
function setAccess(actorId, args) {
|
|
107
|
+
const artifactId = needString(args, 'artifactId');
|
|
108
|
+
const action = needString(args, 'action');
|
|
109
|
+
if (action === 'grant') {
|
|
110
|
+
const grant = needString(args, 'grant');
|
|
111
|
+
if (grant !== 'read' && grant !== 'source') {
|
|
112
|
+
throw new Error('invalid request: grant must be read or source');
|
|
113
|
+
}
|
|
114
|
+
return call(actorId, 'POST', '/access/grant', {
|
|
115
|
+
artifactId, principalId: needString(args, 'principalId'), grant,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (action === 'revoke') {
|
|
119
|
+
return call(actorId, 'POST', '/access/revoke', {
|
|
120
|
+
artifactId, principalId: needString(args, 'principalId'),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (action === 'set-public') {
|
|
124
|
+
if (typeof args['isPublic'] !== 'boolean') {
|
|
125
|
+
throw new Error('invalid request: isPublic must be a boolean');
|
|
126
|
+
}
|
|
127
|
+
return call(actorId, 'POST', '/access/public', { artifactId, isPublic: args['isPublic'] });
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`invalid request: unknown set-access action ${action}`);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
async invoke(input) {
|
|
133
|
+
const { actorId, args } = input;
|
|
134
|
+
switch (input.tool) {
|
|
135
|
+
case 'artifacts_get_template':
|
|
136
|
+
return call(actorId, 'GET', `/templates/${needString(args, 'templateId')}`);
|
|
137
|
+
case 'artifacts_get_api_guide': {
|
|
138
|
+
const version = optString(args, 'version');
|
|
139
|
+
return call(actorId, 'GET', version === undefined ? '/api-guide' : `/api-guide/${version}`);
|
|
140
|
+
}
|
|
141
|
+
case 'artifacts_ensure':
|
|
142
|
+
return call(actorId, 'POST', '/ensure', {
|
|
143
|
+
bindingName: needString(args, 'bindingName'),
|
|
144
|
+
storageScopeId: optString(args, 'storageScopeId') ?? deps.defaultStorageScopeId
|
|
145
|
+
?? needString(args, 'storageScopeId'),
|
|
146
|
+
idempotencyKey: needString(args, 'idempotencyKey'),
|
|
147
|
+
paramsHash: optString(args, 'paramsHash') ?? '',
|
|
148
|
+
parameters: args['parameters'] ?? {},
|
|
149
|
+
});
|
|
150
|
+
case 'artifacts_publish':
|
|
151
|
+
return call(actorId, 'POST', '/publish', {
|
|
152
|
+
artifactId: needString(args, 'artifactId'),
|
|
153
|
+
files: args['files'],
|
|
154
|
+
profile: args['profile'] ?? {},
|
|
155
|
+
source: args['source'],
|
|
156
|
+
idempotencyKey: optString(args, 'idempotencyKey'),
|
|
157
|
+
paramsHash: optString(args, 'paramsHash'),
|
|
158
|
+
});
|
|
159
|
+
case 'artifacts_create_upload':
|
|
160
|
+
return call(actorId, 'POST', '/uploads', {
|
|
161
|
+
artifactId: needString(args, 'artifactId'),
|
|
162
|
+
mimeType: needString(args, 'mimeType'),
|
|
163
|
+
maxSize: args['maxSize'],
|
|
164
|
+
});
|
|
165
|
+
case 'artifacts_commit_upload':
|
|
166
|
+
return call(actorId, 'POST', '/uploads/commit', {
|
|
167
|
+
artifactId: needString(args, 'artifactId'),
|
|
168
|
+
uploadId: needString(args, 'uploadId'),
|
|
169
|
+
path: needString(args, 'path'),
|
|
170
|
+
mimeType: needString(args, 'mimeType'),
|
|
171
|
+
digest: needString(args, 'digest'),
|
|
172
|
+
size: args['size'],
|
|
173
|
+
profile: args['profile'] ?? {},
|
|
174
|
+
idempotencyKey: optString(args, 'idempotencyKey'),
|
|
175
|
+
paramsHash: optString(args, 'paramsHash'),
|
|
176
|
+
});
|
|
177
|
+
case 'artifacts_list':
|
|
178
|
+
return call(actorId, 'POST', '/list', { limit: args['limit'], offset: args['offset'] });
|
|
179
|
+
case 'artifacts_get':
|
|
180
|
+
return call(actorId, 'GET', `/meta/${needString(args, 'artifactId')}`);
|
|
181
|
+
case 'artifacts_get_source':
|
|
182
|
+
return call(actorId, 'GET', `/source/${needString(args, 'artifactId')}`);
|
|
183
|
+
case 'artifacts_fork':
|
|
184
|
+
return call(actorId, 'POST', '/fork', { artifactId: needString(args, 'artifactId') });
|
|
185
|
+
case 'artifacts_set_access':
|
|
186
|
+
return setAccess(actorId, args);
|
|
187
|
+
case 'artifacts_promote':
|
|
188
|
+
return call(actorId, 'POST', '/promote', {
|
|
189
|
+
artifactId: needString(args, 'artifactId'),
|
|
190
|
+
releaseId: needString(args, 'releaseId'),
|
|
191
|
+
expectedCurrent: needString(args, 'expectedCurrent'),
|
|
192
|
+
});
|
|
193
|
+
case 'artifacts_renew':
|
|
194
|
+
return call(actorId, 'POST', '/renew', { artifactId: needString(args, 'artifactId') });
|
|
195
|
+
case 'artifacts_delete':
|
|
196
|
+
return call(actorId, 'POST', '/delete', { artifactId: needString(args, 'artifactId') });
|
|
197
|
+
case 'artifacts_restore':
|
|
198
|
+
return call(actorId, 'POST', '/restore', { artifactId: needString(args, 'artifactId') });
|
|
199
|
+
default:
|
|
200
|
+
throw new Error(`unknown artifacts tool: ${input.tool}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function errorCodeOf(body) {
|
|
206
|
+
if (typeof body === 'object' && body !== null) {
|
|
207
|
+
const code = body['error'];
|
|
208
|
+
if (typeof code === 'string' && code.length !== 0)
|
|
209
|
+
return code;
|
|
210
|
+
}
|
|
211
|
+
return 'internal_error';
|
|
212
|
+
}
|
|
213
|
+
/** Create the platform adapter driving the 15 tools over the shared app. */
|
|
214
|
+
export function createArtifactsAdapter(options) {
|
|
215
|
+
const manifest = createArtifactsManifest(options);
|
|
216
|
+
const executor = createArtifactsToolExecutor({
|
|
217
|
+
app: options.app,
|
|
218
|
+
defaultStorageScopeId: options.defaultStorageScopeId,
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
appId: ARTIFACTS_APP_ID,
|
|
222
|
+
connectorId: ARTIFACTS_PROVIDER_ID,
|
|
223
|
+
manifest,
|
|
224
|
+
resolveTenant: (input) => Promise.resolve({
|
|
225
|
+
principalRef: input.principalSub,
|
|
226
|
+
tenantRef: input.tenantRef,
|
|
227
|
+
connectorInstanceId: input.connectorInstanceId,
|
|
228
|
+
...(input.workspaceRef === undefined ? {} : { workspaceRef: input.workspaceRef }),
|
|
229
|
+
}),
|
|
230
|
+
listCapabilities: () => Promise.resolve([...manifest.tools]),
|
|
231
|
+
async invokeTool(req) {
|
|
232
|
+
const auditId = req.ctx.auditId;
|
|
233
|
+
try {
|
|
234
|
+
const args = typeof req.input === 'object' && req.input !== null && !Array.isArray(req.input)
|
|
235
|
+
? req.input
|
|
236
|
+
: {};
|
|
237
|
+
const res = await executor.invoke({
|
|
238
|
+
tool: req.capabilityRef,
|
|
239
|
+
actorId: req.ctx.principal.sub,
|
|
240
|
+
args,
|
|
241
|
+
});
|
|
242
|
+
if (res.status >= 200 && res.status < 300) {
|
|
243
|
+
return { ok: true, output: res.body, auditId, redactionClass: 'low' };
|
|
244
|
+
}
|
|
245
|
+
const code = errorCodeOf(res.body);
|
|
246
|
+
return {
|
|
247
|
+
ok: false, auditId, redactionClass: 'low',
|
|
248
|
+
error: { code, message: code, retriable: res.status >= 500 },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return {
|
|
253
|
+
ok: false, auditId, redactionClass: 'low',
|
|
254
|
+
error: { code: 'invalid_request', message: 'invalid_request', retriable: false },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
readResource: (req) => Promise.resolve({
|
|
259
|
+
ok: false,
|
|
260
|
+
auditId: req.ctx.auditId,
|
|
261
|
+
redactionClass: 'low',
|
|
262
|
+
error: { code: 'not_found', message: 'not_found', retriable: false },
|
|
263
|
+
}),
|
|
264
|
+
validateSecrets: () => Promise.resolve([]),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const EMPTY_TOOL_CONFIG = { type: 'object', properties: {}, additionalProperties: false };
|
|
268
|
+
/** Build the kernel registry definition for sentropic.artifacts (§3). */
|
|
269
|
+
export function createArtifactsRegistryDefinition(options) {
|
|
270
|
+
const adapter = createArtifactsAdapter(options);
|
|
271
|
+
const manifest = adapter.manifest;
|
|
272
|
+
const handlers = {};
|
|
273
|
+
const toolValues = {};
|
|
274
|
+
for (const spec of ARTIFACTS_TOOL_SPECS) {
|
|
275
|
+
const handlerId = `${spec.name}-handler`;
|
|
276
|
+
handlers[handlerId] = { handlerId, capabilityRef: spec.name, configSchema: EMPTY_TOOL_CONFIG };
|
|
277
|
+
toolValues[spec.name] = { handlerId, values: {} };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
manifest,
|
|
281
|
+
adapter,
|
|
282
|
+
parameterSchema: ARTIFACTS_PARAMETER_SCHEMA,
|
|
283
|
+
handlers,
|
|
284
|
+
requiredPorts: [],
|
|
285
|
+
provenance: {
|
|
286
|
+
approvalRef: options.approvalRef ?? 'artifacts-local-1',
|
|
287
|
+
revision: options.revision ?? 'rev-1',
|
|
288
|
+
manifestDigest: computeManifestDigest(manifest),
|
|
289
|
+
reviewedBy: options.reviewedBy ?? ['host-owner'],
|
|
290
|
+
},
|
|
291
|
+
toolValues,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5a — provider parameter schema and validation (spec-v8 §9).
|
|
3
|
+
*
|
|
4
|
+
* Generic, non-secret provider settings: each declaration names a key, its
|
|
5
|
+
* type and whether it is required or defaulted. Validation applies defaults
|
|
6
|
+
* and rejects unknown keys, wrong types, missing required values and
|
|
7
|
+
* secret-looking keys fail-closed — provider parameters are never a secret
|
|
8
|
+
* store. Schemas are independent values; no global bag is shared.
|
|
9
|
+
*/
|
|
10
|
+
import type { JsonValue } from './contracts.js';
|
|
11
|
+
export type ParameterType = 'string' | 'number' | 'boolean' | 'enum';
|
|
12
|
+
export interface ParameterDeclaration {
|
|
13
|
+
key: string;
|
|
14
|
+
type: ParameterType;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
default?: JsonValue;
|
|
17
|
+
values?: string[];
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ParameterSchema {
|
|
21
|
+
declarations: ParameterDeclaration[];
|
|
22
|
+
}
|
|
23
|
+
/** Declare a parameter schema; rejects duplicates and empty declarations. */
|
|
24
|
+
export declare function defineParameters(declarations: ParameterDeclaration[]): ParameterSchema;
|
|
25
|
+
/**
|
|
26
|
+
* Validate values against a schema: apply defaults, reject unknown keys,
|
|
27
|
+
* wrong types, missing required values and secret-looking keys.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateParameters(schema: ParameterSchema, values: Record<string, JsonValue | undefined>): Record<string, JsonValue>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Keys that look like secrets are refused anywhere in validated values. */
|
|
2
|
+
const SECRET_KEY_PATTERN = /(secret|token|password|private[_-]?key|api[_-]?key)/i;
|
|
3
|
+
/** Declare a parameter schema; rejects duplicates and empty declarations. */
|
|
4
|
+
export function defineParameters(declarations) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
for (const decl of declarations) {
|
|
7
|
+
if (typeof decl.key !== 'string' || decl.key.length === 0) {
|
|
8
|
+
throw new Error('invalid parameter declaration: empty key');
|
|
9
|
+
}
|
|
10
|
+
if (seen.has(decl.key)) {
|
|
11
|
+
throw new Error(`invalid parameter declaration: duplicate key ${decl.key}`);
|
|
12
|
+
}
|
|
13
|
+
seen.add(decl.key);
|
|
14
|
+
if (decl.type === 'enum' && (!Array.isArray(decl.values) || decl.values.length === 0)) {
|
|
15
|
+
throw new Error(`invalid parameter declaration: enum ${decl.key} needs values`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { declarations: [...declarations] };
|
|
19
|
+
}
|
|
20
|
+
function checkType(decl, value) {
|
|
21
|
+
switch (decl.type) {
|
|
22
|
+
case 'string':
|
|
23
|
+
return typeof value === 'string';
|
|
24
|
+
case 'number':
|
|
25
|
+
return typeof value === 'number';
|
|
26
|
+
case 'boolean':
|
|
27
|
+
return typeof value === 'boolean';
|
|
28
|
+
case 'enum':
|
|
29
|
+
return typeof value === 'string' && (decl.values ?? []).includes(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate values against a schema: apply defaults, reject unknown keys,
|
|
34
|
+
* wrong types, missing required values and secret-looking keys.
|
|
35
|
+
*/
|
|
36
|
+
export function validateParameters(schema, values) {
|
|
37
|
+
for (const key of Object.keys(values)) {
|
|
38
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
39
|
+
throw new Error(`invalid parameters: secret-looking key refused: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const known = new Map(schema.declarations.map((d) => [d.key, d]));
|
|
43
|
+
for (const key of Object.keys(values)) {
|
|
44
|
+
if (values[key] !== undefined && !known.has(key)) {
|
|
45
|
+
throw new Error(`invalid parameters: unknown parameter ${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const decl of schema.declarations) {
|
|
50
|
+
const value = values[decl.key];
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
if (decl.required) {
|
|
53
|
+
throw new Error(`invalid parameters: missing required parameter ${decl.key}`);
|
|
54
|
+
}
|
|
55
|
+
if (decl.default !== undefined)
|
|
56
|
+
out[decl.key] = decl.default;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!checkType(decl, value)) {
|
|
60
|
+
throw new Error(`invalid parameters: wrong type for ${decl.key}`);
|
|
61
|
+
}
|
|
62
|
+
out[decl.key] = value;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5a — generic provider definitions (spec-v8 §9).
|
|
3
|
+
*
|
|
4
|
+
* A provider definition binds a provider ID to its capabilities and its
|
|
5
|
+
* parameter schema. Provider-internal keys are (providerId, capabilityName);
|
|
6
|
+
* the external map is exact publicName → that pair. Duplicate public names
|
|
7
|
+
* are rejected at definition time; cross-provider collisions are rejected by
|
|
8
|
+
* `assertNoPublicNameCollisions` before activation. No sanitizing,
|
|
9
|
+
* suffixing, auto-prefixing or silent first/last keep.
|
|
10
|
+
*/
|
|
11
|
+
import type { ParameterSchema } from './parameters.js';
|
|
12
|
+
export type CapabilityKind = 'tool' | 'resource' | 'prompt';
|
|
13
|
+
export interface CapabilityDefinition {
|
|
14
|
+
kind: CapabilityKind;
|
|
15
|
+
/** Internal capability name, unique within the provider. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Exact external name; must be unique across all activated providers. */
|
|
18
|
+
publicName: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderDefinition {
|
|
22
|
+
providerId: string;
|
|
23
|
+
version: string;
|
|
24
|
+
parameters: ParameterSchema;
|
|
25
|
+
capabilities: CapabilityDefinition[];
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedCapability {
|
|
28
|
+
providerId: string;
|
|
29
|
+
capabilityName: string;
|
|
30
|
+
}
|
|
31
|
+
/** Define a provider; rejects empty IDs and duplicate public/capability names. */
|
|
32
|
+
export declare function defineProvider(def: ProviderDefinition): ProviderDefinition;
|
|
33
|
+
/** Exact publicName → (providerId, capabilityName) map for one provider. */
|
|
34
|
+
export declare function publicNameMap(def: ProviderDefinition): Map<string, ResolvedCapability>;
|
|
35
|
+
/** Reject tool-name collisions across providers before activation. */
|
|
36
|
+
export declare function assertNoPublicNameCollisions(defs: ProviderDefinition[]): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Define a provider; rejects empty IDs and duplicate public/capability names. */
|
|
2
|
+
export function defineProvider(def) {
|
|
3
|
+
if (typeof def.providerId !== 'string' || def.providerId.length === 0) {
|
|
4
|
+
throw new Error('invalid provider definition: empty providerId');
|
|
5
|
+
}
|
|
6
|
+
const publicNames = new Set();
|
|
7
|
+
const names = new Set();
|
|
8
|
+
for (const cap of def.capabilities) {
|
|
9
|
+
if (names.has(cap.name)) {
|
|
10
|
+
throw new Error(`invalid provider definition: duplicate capability name ${cap.name}`);
|
|
11
|
+
}
|
|
12
|
+
names.add(cap.name);
|
|
13
|
+
if (publicNames.has(cap.publicName)) {
|
|
14
|
+
throw new Error(`invalid provider definition: duplicate publicName ${cap.publicName}`);
|
|
15
|
+
}
|
|
16
|
+
publicNames.add(cap.publicName);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
providerId: def.providerId,
|
|
20
|
+
version: def.version,
|
|
21
|
+
parameters: def.parameters,
|
|
22
|
+
capabilities: [...def.capabilities],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Exact publicName → (providerId, capabilityName) map for one provider. */
|
|
26
|
+
export function publicNameMap(def) {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const cap of def.capabilities) {
|
|
29
|
+
map.set(cap.publicName, { providerId: def.providerId, capabilityName: cap.name });
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
/** Reject tool-name collisions across providers before activation. */
|
|
34
|
+
export function assertNoPublicNameCollisions(defs) {
|
|
35
|
+
const seen = new Map();
|
|
36
|
+
for (const def of defs) {
|
|
37
|
+
for (const cap of def.capabilities) {
|
|
38
|
+
const owner = seen.get(cap.publicName);
|
|
39
|
+
if (owner !== undefined && owner !== def.providerId) {
|
|
40
|
+
throw new Error(`publicName collision: ${cap.publicName} claimed by ${owner} and ${def.providerId}`);
|
|
41
|
+
}
|
|
42
|
+
seen.set(cap.publicName, def.providerId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5c — access control (spec-v8 §9 5c).
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation over an artifact ACL: the owner reads, exports source and
|
|
5
|
+
* manages grants; explicit grants give read or source (source implies read,
|
|
6
|
+
* read never implies source); a public flag opens reads to approved accounts
|
|
7
|
+
* only — never anonymous, never source. Withdrawal suspends reader grants
|
|
8
|
+
* (kept for audit, inactive) while the owner keeps access. Grant management
|
|
9
|
+
* is owner-only; duplicate grants are refused. Identity approval comes from
|
|
10
|
+
* the host via an injected predicate (memory double in tests).
|
|
11
|
+
*/
|
|
12
|
+
export type AccessGrant = 'read' | 'source';
|
|
13
|
+
export type AccessAction = 'read' | 'source';
|
|
14
|
+
export interface GrantRecord {
|
|
15
|
+
principalId: string;
|
|
16
|
+
grant: AccessGrant;
|
|
17
|
+
}
|
|
18
|
+
export interface ArtifactAcl {
|
|
19
|
+
ownerId: string;
|
|
20
|
+
/** Physical scope of the artifact (set at creation; read by publish). */
|
|
21
|
+
storageScopeId?: string;
|
|
22
|
+
isPublic: boolean;
|
|
23
|
+
withdrawn: boolean;
|
|
24
|
+
grants: GrantRecord[];
|
|
25
|
+
}
|
|
26
|
+
export interface AclStore {
|
|
27
|
+
getArtifactAcl(artifactId: string): Promise<ArtifactAcl | undefined>;
|
|
28
|
+
putArtifactAcl(artifactId: string, acl: ArtifactAcl): Promise<void>;
|
|
29
|
+
/** All known artifact IDs (stable creation order); filter by ACL first. */
|
|
30
|
+
listArtifactIds(): Promise<string[]>;
|
|
31
|
+
}
|
|
32
|
+
/** In-memory AclStore double for unit tests. */
|
|
33
|
+
export declare function createMemoryAclStore(): AclStore;
|
|
34
|
+
/** True when the actor may perform the action (deny by default). */
|
|
35
|
+
export declare function evaluateAccess(acl: ArtifactAcl, actorId: string, action: AccessAction, isApproved?: (principalId: string) => boolean): boolean;
|
|
36
|
+
/** Owner-only: add a grant (duplicates refused). */
|
|
37
|
+
export declare function grantAccess(acl: ArtifactAcl, actorId: string, grant: GrantRecord): ArtifactAcl;
|
|
38
|
+
/** Owner-only: remove a grant (kept for audit by the caller if needed). */
|
|
39
|
+
export declare function revokeAccess(acl: ArtifactAcl, actorId: string, principalId: string): ArtifactAcl;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** In-memory AclStore double for unit tests. */
|
|
2
|
+
export function createMemoryAclStore() {
|
|
3
|
+
const acls = new Map();
|
|
4
|
+
return {
|
|
5
|
+
getArtifactAcl(artifactId) {
|
|
6
|
+
const acl = acls.get(artifactId);
|
|
7
|
+
return Promise.resolve(acl === undefined
|
|
8
|
+
? undefined
|
|
9
|
+
: { ...acl, grants: acl.grants.map((g) => ({ ...g })) });
|
|
10
|
+
},
|
|
11
|
+
putArtifactAcl(artifactId, acl) {
|
|
12
|
+
acls.set(artifactId, { ...acl, grants: acl.grants.map((g) => ({ ...g })) });
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
},
|
|
15
|
+
listArtifactIds() {
|
|
16
|
+
return Promise.resolve([...acls.keys()]);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** True when the actor may perform the action (deny by default). */
|
|
21
|
+
export function evaluateAccess(acl, actorId, action, isApproved = () => false) {
|
|
22
|
+
if (actorId.length === 0)
|
|
23
|
+
return false;
|
|
24
|
+
if (actorId === acl.ownerId)
|
|
25
|
+
return true;
|
|
26
|
+
if (acl.withdrawn)
|
|
27
|
+
return false;
|
|
28
|
+
const grant = acl.grants.find((g) => g.principalId === actorId)?.grant;
|
|
29
|
+
if (grant === 'source')
|
|
30
|
+
return true;
|
|
31
|
+
if (grant === 'read' && action === 'read')
|
|
32
|
+
return true;
|
|
33
|
+
if (action === 'read' && acl.isPublic && isApproved(actorId))
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/** Owner-only: add a grant (duplicates refused). */
|
|
38
|
+
export function grantAccess(acl, actorId, grant) {
|
|
39
|
+
if (actorId !== acl.ownerId)
|
|
40
|
+
throw new Error('access denied: only the owner manages grants');
|
|
41
|
+
if (grant.principalId.length === 0)
|
|
42
|
+
throw new Error('invalid grant: empty principal');
|
|
43
|
+
if (acl.grants.some((g) => g.principalId === grant.principalId)) {
|
|
44
|
+
throw new Error(`duplicate grant for ${grant.principalId}`);
|
|
45
|
+
}
|
|
46
|
+
return { ...acl, grants: [...acl.grants, grant] };
|
|
47
|
+
}
|
|
48
|
+
/** Owner-only: remove a grant (kept for audit by the caller if needed). */
|
|
49
|
+
export function revokeAccess(acl, actorId, principalId) {
|
|
50
|
+
if (actorId !== acl.ownerId)
|
|
51
|
+
throw new Error('access denied: only the owner manages grants');
|
|
52
|
+
return { ...acl, grants: acl.grants.filter((g) => g.principalId !== principalId) };
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5a — object-level authorization (spec-v8 §9).
|
|
3
|
+
*
|
|
4
|
+
* Private artifacts are owner-only: the acting user must equal the recorded
|
|
5
|
+
* artifact owner for both read and mutate. Anything else — forged owner,
|
|
6
|
+
* anonymous actor, empty owner — is denied fail-closed. There are no sharing
|
|
7
|
+
* grants in this lot (sharing arrives with L-5c access.ts), so the action
|
|
8
|
+
* set is closed: unknown actions are refused rather than defaulted.
|
|
9
|
+
*/
|
|
10
|
+
export type ArtifactAction = 'read' | 'mutate';
|
|
11
|
+
export interface AccessRequest {
|
|
12
|
+
actorUserId: string;
|
|
13
|
+
artifactOwnerId: string;
|
|
14
|
+
action: string;
|
|
15
|
+
}
|
|
16
|
+
/** Assert owner-only access; throws fail-closed on any mismatch. */
|
|
17
|
+
export declare function assertArtifactAccess(request: AccessRequest): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Assert owner-only access; throws fail-closed on any mismatch. */
|
|
2
|
+
export function assertArtifactAccess(request) {
|
|
3
|
+
if (request.action !== 'read' && request.action !== 'mutate') {
|
|
4
|
+
throw new Error(`access denied: unknown action ${request.action}`);
|
|
5
|
+
}
|
|
6
|
+
if (request.actorUserId.length === 0 || request.artifactOwnerId.length === 0) {
|
|
7
|
+
throw new Error('access denied: anonymous actor or empty owner');
|
|
8
|
+
}
|
|
9
|
+
if (request.actorUserId !== request.artifactOwnerId) {
|
|
10
|
+
throw new Error('access denied: not the artifact owner');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5a — owner-scoped stable bindings (spec-v8 §9).
|
|
3
|
+
*
|
|
4
|
+
* A logical binding (owner + name) resolves to one server-allocated UUID no
|
|
5
|
+
* matter which client ensures it first; both fresh clients converge on the
|
|
6
|
+
* same identity. Another owner resolves nothing: lookup is always qualified
|
|
7
|
+
* by ownerId, so binding names and digests cannot be guessed across owners.
|
|
8
|
+
* A conflicting storage scope on an existing binding fails closed (mappings
|
|
9
|
+
* stay server-verified; ambiguous mappings never silently win). Concurrent
|
|
10
|
+
* ensures serialize on a per-key singleflight so exactly one creation wins.
|
|
11
|
+
* Persistence goes through the injected KvPort only (ports.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { type KvPort, type UuidPort } from './ports.js';
|
|
14
|
+
export interface BindingInput {
|
|
15
|
+
ownerId: string;
|
|
16
|
+
storageScopeId: string;
|
|
17
|
+
bindingName: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BindingLookup {
|
|
20
|
+
ownerId: string;
|
|
21
|
+
bindingName: string;
|
|
22
|
+
}
|
|
23
|
+
export interface BindingResult {
|
|
24
|
+
artifactId: string;
|
|
25
|
+
created: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface BindingService {
|
|
28
|
+
ensure(input: BindingInput): Promise<BindingResult>;
|
|
29
|
+
find(lookup: BindingLookup): Promise<string | undefined>;
|
|
30
|
+
}
|
|
31
|
+
/** Memory-backed KvPort for unit tests (re-exported from the ports module). */
|
|
32
|
+
export declare function createMemoryBindingKv(): KvPort;
|
|
33
|
+
/** Create a binding service over an injected KvPort (no driver imports). */
|
|
34
|
+
export declare function createBindingService(kv: KvPort, uuid?: UuidPort): BindingService;
|