@ciphrix/cli 0.1.0 → 0.2.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/README.md +47 -4
- package/dist/actions.d.ts +3 -1
- package/dist/actions.js +16 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +7 -3
- package/dist/cli.js +47 -8
- package/dist/commands/asset.d.ts +4 -1
- package/dist/commands/asset.js +15 -8
- package/dist/commands/capabilities.d.ts +6 -0
- package/dist/commands/capabilities.js +288 -0
- package/dist/commands/capabilityDefinitions.d.ts +17 -0
- package/dist/commands/capabilityDefinitions.js +191 -0
- package/dist/commands/context.js +9 -7
- package/dist/commands/control.d.ts +3 -1
- package/dist/commands/control.js +26 -1
- package/dist/commands/framework.d.ts +3 -1
- package/dist/commands/framework.js +20 -4
- package/dist/commands/policy.js +1 -1
- package/dist/commands/test.d.ts +5 -3
- package/dist/commands/test.js +33 -4
- package/dist/commands/vendor.d.ts +4 -1
- package/dist/commands/vendor.js +7 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/manifest.d.ts +19 -0
- package/dist/manifest.js +93 -0
- package/dist/toolSurface.js +10 -1
- package/dist/upload.d.ts +11 -1
- package/dist/upload.js +33 -13
- package/package.json +1 -1
- package/skills/ciphrix/SKILL.md +26 -1
- package/skills/ciphrix/references/commands.md +1261 -24
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeLine, writeNextHint, writeTable } from '../io.js';
|
|
1
|
+
import { writeLine, writeNextHint, writePageFooter, writeTable } from '../io.js';
|
|
2
2
|
import { asRecord, callTool, errorMessage, resolveToolContext, asText } from '../toolSurface.js';
|
|
3
3
|
const text = (value, fallback = '-') => typeof value === 'string' ? value : typeof value === 'number' ? String(value) : fallback;
|
|
4
4
|
export const frameworkListCommand = async ({ io, apiUrl, json = false, credentialStore, }) => {
|
|
@@ -34,7 +34,8 @@ export const frameworkListCommand = async ({ io, apiUrl, json = false, credentia
|
|
|
34
34
|
writeLine(io.stdout, `${io.theme.dim(`Available to add (${available.length}):`)}`);
|
|
35
35
|
for (const framework of available) {
|
|
36
36
|
const record = asRecord(framework);
|
|
37
|
-
|
|
37
|
+
const code = asText(record.frameworkCode ?? record.code);
|
|
38
|
+
writeLine(io.stdout, ` ${code ? `${code} — ` : ''}${text(record.frameworkName ?? record.name)}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
};
|
|
@@ -118,11 +119,15 @@ export const clauseItemsCommand = async ({ io, framework, clause, apiUrl, json =
|
|
|
118
119
|
return [text(record.name, text(record.id)), text(record.itemType ?? record.type)];
|
|
119
120
|
}));
|
|
120
121
|
};
|
|
121
|
-
export const clauseAvailableItemsCommand = async ({ io, framework, clause, itemType, apiUrl, json = false, credentialStore, search, }) => {
|
|
122
|
+
export const clauseAvailableItemsCommand = async ({ io, framework, clause, itemType, apiUrl, json = false, credentialStore, search, page, limit, }) => {
|
|
122
123
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
123
124
|
const input = { framework, clause, itemType };
|
|
124
125
|
if (search)
|
|
125
126
|
input.search = search;
|
|
127
|
+
if (page)
|
|
128
|
+
input.page = page;
|
|
129
|
+
if (limit)
|
|
130
|
+
input.limit = limit;
|
|
126
131
|
const envelope = await callTool(ctx, 'list_clause_items_available', input);
|
|
127
132
|
if (envelope.status !== 'ok')
|
|
128
133
|
throw new Error(errorMessage(envelope, 'Could not read available items.'));
|
|
@@ -130,13 +135,24 @@ export const clauseAvailableItemsCommand = async ({ io, framework, clause, itemT
|
|
|
130
135
|
writeLine(io.stdout, JSON.stringify(envelope.data, null, 2));
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
|
-
const
|
|
138
|
+
const data = asRecord(envelope.data);
|
|
139
|
+
const items = data.items;
|
|
134
140
|
const list = Array.isArray(items) ? items : [];
|
|
135
141
|
writeLine(io.stdout, `${io.theme.bold(`Available ${itemType}s`)} ${io.theme.dim(`(${list.length})`)}`);
|
|
136
142
|
for (const item of list) {
|
|
137
143
|
const record = asRecord(item);
|
|
138
144
|
writeLine(io.stdout, ` ${text(record.name, text(record.id))}`);
|
|
139
145
|
}
|
|
146
|
+
const total = typeof data.total === 'number' ? data.total : list.length;
|
|
147
|
+
const currentPage = typeof data.page === 'number' ? data.page : 1;
|
|
148
|
+
const pageSize = typeof data.limit === 'number' ? data.limit : 25;
|
|
149
|
+
writePageFooter(io, {
|
|
150
|
+
shown: list.length,
|
|
151
|
+
total,
|
|
152
|
+
page: currentPage,
|
|
153
|
+
limit: pageSize,
|
|
154
|
+
hasMore: data.hasMore === true || currentPage * pageSize < total,
|
|
155
|
+
});
|
|
140
156
|
};
|
|
141
157
|
export const clauseUpdateCommand = async ({ io, framework, clause, changes, apiUrl, json = false, yes = false, credentialStore, }) => {
|
|
142
158
|
const { applyTool } = await import('../actions.js');
|
package/dist/commands/policy.js
CHANGED
|
@@ -20,7 +20,7 @@ export const policyListCommand = async ({ io, apiUrl, json = false, name, status
|
|
|
20
20
|
if (envelope.status !== 'ok')
|
|
21
21
|
throw new Error(errorMessage(envelope, 'Could not list documents.'));
|
|
22
22
|
if (json) {
|
|
23
|
-
writeLine(io.stdout, JSON.stringify(envelope, null, 2));
|
|
23
|
+
writeLine(io.stdout, JSON.stringify(envelope.data, null, 2));
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
const data = asRecord(envelope.data);
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export declare const testRunCreateCommand: ({ io, name, apiUrl, json, credential
|
|
|
29
29
|
runAt?: string | undefined;
|
|
30
30
|
}) => Promise<void>;
|
|
31
31
|
export declare const testRunResultCommand: ({ io, name, runId, apiUrl, json, credentialStore, status, note, }: TestReadOptions & {
|
|
32
|
-
status
|
|
33
|
-
note?: string | undefined;
|
|
32
|
+
status?: string | undefined;
|
|
33
|
+
note?: string | null | undefined;
|
|
34
34
|
}) => Promise<void>;
|
|
35
35
|
export declare const testRunDeleteCommand: ({ io, name, runId, apiUrl, json, yes, confirm, credentialStore, }: TestActionOptions) => Promise<void>;
|
|
36
36
|
export interface TestAttachOptions extends TestReadOptions {
|
|
@@ -41,9 +41,11 @@ export interface TestAttachOptions extends TestReadOptions {
|
|
|
41
41
|
unlink?: boolean | undefined;
|
|
42
42
|
}
|
|
43
43
|
export declare const testAttachCommand: ({ io, name, runId, apiUrl, json, credentialStore, itemType, itemId, uploadIds, file, unlink, }: TestAttachOptions) => Promise<void>;
|
|
44
|
-
export declare const testAvailableItemsCommand: ({ io, name, runId, itemType, apiUrl, json, credentialStore, search, }: TestReadOptions & {
|
|
44
|
+
export declare const testAvailableItemsCommand: ({ io, name, runId, itemType, apiUrl, json, credentialStore, search, page, limit, }: TestReadOptions & {
|
|
45
45
|
itemType: string;
|
|
46
46
|
search?: string | undefined;
|
|
47
|
+
page?: number | undefined;
|
|
48
|
+
limit?: number | undefined;
|
|
47
49
|
}) => Promise<void>;
|
|
48
50
|
export declare const testLinksCommand: ({ io, name, apiUrl, json, credentialStore, }: TestReadOptions) => Promise<void>;
|
|
49
51
|
export declare const testUpdateCommand: ({ io, name, apiUrl, json, yes, confirm, credentialStore, changes, }: TestReadOptions & {
|
package/dist/commands/test.js
CHANGED
|
@@ -113,6 +113,12 @@ const renderRuns = (io, runs, assurance) => {
|
|
|
113
113
|
assuranceLabel(assurance[text(record.id)]),
|
|
114
114
|
];
|
|
115
115
|
}));
|
|
116
|
+
for (const run of runs) {
|
|
117
|
+
const record = asRecord(run);
|
|
118
|
+
if (typeof record.note === 'string' && record.note.trim() !== '') {
|
|
119
|
+
writeLine(io.stdout, `Note (${text(record.id)}): ${record.note}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
116
122
|
};
|
|
117
123
|
const assuranceLabel = (assessment) => assessmentLabel(assessment);
|
|
118
124
|
export const testItemsCommand = async ({ io, name, runId, apiUrl, json = false, credentialStore, }) => {
|
|
@@ -162,11 +168,15 @@ export const testRunCreateCommand = async ({ io, name, apiUrl, json = false, cre
|
|
|
162
168
|
writeLine(io.stdout, `${io.theme.green('✓')} run created: ${text(asRecord(result.data).runId)}`);
|
|
163
169
|
};
|
|
164
170
|
export const testRunResultCommand = async ({ io, name, runId, apiUrl, json = false, credentialStore, status, note, }) => {
|
|
171
|
+
if (status === undefined && note === undefined)
|
|
172
|
+
throw new Error('Provide --status, --note or --clear-note.');
|
|
165
173
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
166
|
-
const input = { name
|
|
174
|
+
const input = { name };
|
|
175
|
+
if (status !== undefined)
|
|
176
|
+
input.status = status;
|
|
167
177
|
if (runId)
|
|
168
178
|
input.runId = runId;
|
|
169
|
-
if (note)
|
|
179
|
+
if (note !== undefined)
|
|
170
180
|
input.note = note;
|
|
171
181
|
const result = await applyTool({
|
|
172
182
|
ctx,
|
|
@@ -180,7 +190,12 @@ export const testRunResultCommand = async ({ io, name, runId, apiUrl, json = fal
|
|
|
180
190
|
if (json)
|
|
181
191
|
writeLine(io.stdout, JSON.stringify(result.data, null, 2));
|
|
182
192
|
else
|
|
183
|
-
writeLine(io.stdout,
|
|
193
|
+
writeLine(io.stdout, status === undefined
|
|
194
|
+
? 'Run note updated; result unchanged.'
|
|
195
|
+
: `${io.theme.green('✓')} run result: ${status}`);
|
|
196
|
+
const jobId = asText(asRecord(result.meta).aiTaskId);
|
|
197
|
+
if (!json && jobId)
|
|
198
|
+
writeNextHint(io, `ciphrix job status ${jobId}`);
|
|
184
199
|
};
|
|
185
200
|
export const testRunDeleteCommand = async ({ io, name, runId, apiUrl, json = false, yes = false, confirm, credentialStore, }) => {
|
|
186
201
|
if (!runId)
|
|
@@ -235,13 +250,17 @@ export const testAttachCommand = async ({ io, name, runId, apiUrl, json = false,
|
|
|
235
250
|
const job = asText(data.aiTaskId);
|
|
236
251
|
writeLine(io.stdout, `${io.theme.green('✓')} ${unlink ? 'unlinked' : 'linked'} ${resolvedItemType} on run ${text(data.runId)}${job ? io.theme.dim(` · job ${job}`) : ''}`);
|
|
237
252
|
};
|
|
238
|
-
export const testAvailableItemsCommand = async ({ io, name, runId, itemType, apiUrl, json = false, credentialStore, search, }) => {
|
|
253
|
+
export const testAvailableItemsCommand = async ({ io, name, runId, itemType, apiUrl, json = false, credentialStore, search, page, limit, }) => {
|
|
239
254
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
240
255
|
const input = { name, itemType };
|
|
241
256
|
if (runId)
|
|
242
257
|
input.runId = runId;
|
|
243
258
|
if (search)
|
|
244
259
|
input.search = search;
|
|
260
|
+
if (page)
|
|
261
|
+
input.page = page;
|
|
262
|
+
if (limit)
|
|
263
|
+
input.limit = limit;
|
|
245
264
|
const envelope = await callTool(ctx, 'list_test_items_available', input);
|
|
246
265
|
if (envelope.status !== 'ok')
|
|
247
266
|
throw new Error(errorMessage(envelope, 'Could not read available items.'));
|
|
@@ -264,6 +283,16 @@ export const testAvailableItemsCommand = async ({ io, name, runId, itemType, api
|
|
|
264
283
|
text(record.status),
|
|
265
284
|
];
|
|
266
285
|
}));
|
|
286
|
+
const total = typeof data.total === 'number' ? data.total : items.length;
|
|
287
|
+
const currentPage = typeof data.page === 'number' ? data.page : 1;
|
|
288
|
+
const pageSize = typeof data.limit === 'number' ? data.limit : 25;
|
|
289
|
+
writePageFooter(io, {
|
|
290
|
+
shown: items.length,
|
|
291
|
+
total,
|
|
292
|
+
page: currentPage,
|
|
293
|
+
limit: pageSize,
|
|
294
|
+
hasMore: data.hasMore === true || currentPage * pageSize < total,
|
|
295
|
+
});
|
|
267
296
|
};
|
|
268
297
|
export const testLinksCommand = async ({ io, name, apiUrl, json = false, credentialStore, }) => {
|
|
269
298
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
@@ -22,10 +22,13 @@ export declare const vendorListCommand: ({ io, apiUrl, json, credentialStore, na
|
|
|
22
22
|
limit?: number | undefined;
|
|
23
23
|
}) => Promise<void>;
|
|
24
24
|
export declare const vendorGetCommand: ({ io, name, apiUrl, json, credentialStore, }: VendorReadOptions) => Promise<void>;
|
|
25
|
-
export declare const vendorCreateCommand: ({ io, apiUrl, json, credentialStore, name, category, criticality, description, }: VendorCommonOptions & {
|
|
25
|
+
export declare const vendorCreateCommand: ({ io, apiUrl, json, credentialStore, name, category, subcategory, criticality, dataSensitivity, serviceAvailability, description, }: VendorCommonOptions & {
|
|
26
26
|
name: string;
|
|
27
27
|
category?: string | undefined;
|
|
28
|
+
subcategory?: string | undefined;
|
|
28
29
|
criticality?: string | undefined;
|
|
30
|
+
dataSensitivity?: string | undefined;
|
|
31
|
+
serviceAvailability?: string | undefined;
|
|
29
32
|
description?: string | undefined;
|
|
30
33
|
}) => Promise<void>;
|
|
31
34
|
export interface VendorUpdateOptions extends VendorReadOptions {
|
package/dist/commands/vendor.js
CHANGED
|
@@ -78,13 +78,19 @@ export const vendorGetCommand = async ({ io, name, apiUrl, json = false, credent
|
|
|
78
78
|
writeLine(io.stdout, ` ${io.theme.dim(`${label}:`)} ${text(value)}`);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
-
export const vendorCreateCommand = async ({ io, apiUrl, json = false, credentialStore, name, category, criticality, description, }) => {
|
|
81
|
+
export const vendorCreateCommand = async ({ io, apiUrl, json = false, credentialStore, name, category, subcategory, criticality, dataSensitivity, serviceAvailability, description, }) => {
|
|
82
82
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
83
83
|
const input = { name };
|
|
84
84
|
if (category)
|
|
85
85
|
input.category = category;
|
|
86
|
+
if (subcategory)
|
|
87
|
+
input.subcategory = subcategory;
|
|
86
88
|
if (criticality)
|
|
87
89
|
input.criticality = criticality;
|
|
90
|
+
if (dataSensitivity)
|
|
91
|
+
input.dataSensitivity = dataSensitivity;
|
|
92
|
+
if (serviceAvailability)
|
|
93
|
+
input.serviceAvailability = serviceAvailability;
|
|
88
94
|
if (description)
|
|
89
95
|
input.description = description;
|
|
90
96
|
const result = await applyTool({
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ToolContext } from './toolSurface.js';
|
|
2
|
+
export interface ToolDescriptor {
|
|
3
|
+
name: string;
|
|
4
|
+
version: number;
|
|
5
|
+
description: string;
|
|
6
|
+
effect: 'read' | 'write' | 'destructive';
|
|
7
|
+
confirmation: 'none' | 'preflight_confirm';
|
|
8
|
+
inputSchema: Record<string, unknown>;
|
|
9
|
+
cli?: {
|
|
10
|
+
group: string[];
|
|
11
|
+
operation: string;
|
|
12
|
+
};
|
|
13
|
+
limits?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare class ManifestError extends Error {
|
|
16
|
+
}
|
|
17
|
+
export declare const getManifest: (ctx: ToolContext) => Promise<ToolDescriptor[]>;
|
|
18
|
+
export declare const getTool: (ctx: ToolContext, name: string) => Promise<ToolDescriptor>;
|
|
19
|
+
export declare const resolveBinding: (ctx: ToolContext, group: string[], operation: string) => Promise<ToolDescriptor>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export class ManifestError extends Error {
|
|
2
|
+
}
|
|
3
|
+
const cache = new WeakMap();
|
|
4
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: {};
|
|
7
|
+
const checkSchema = (value, depth = 0, budget = { properties: 0 }) => {
|
|
8
|
+
if (depth > 32)
|
|
9
|
+
throw new ManifestError('Tool schema exceeds the supported nesting limit.');
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const child of value)
|
|
12
|
+
checkSchema(child, depth + 1, budget);
|
|
13
|
+
}
|
|
14
|
+
else if (value && typeof value === 'object') {
|
|
15
|
+
const schema = record(value);
|
|
16
|
+
budget.properties += Object.keys(record(schema.properties)).length;
|
|
17
|
+
if (budget.properties > 1000)
|
|
18
|
+
throw new ManifestError('Tool schema exceeds the supported property limit.');
|
|
19
|
+
for (const child of Object.values(schema))
|
|
20
|
+
checkSchema(child, depth + 1, budget);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const getManifest = (ctx) => {
|
|
24
|
+
const previous = cache.get(ctx);
|
|
25
|
+
if (previous?.baseUrl === ctx.baseUrl && previous.token === ctx.token)
|
|
26
|
+
return previous.manifest;
|
|
27
|
+
const loaded = (async () => {
|
|
28
|
+
const body = record(await ctx.client.request('/tools/v1', { token: ctx.token }));
|
|
29
|
+
if (body.manifestVersion !== 1 ||
|
|
30
|
+
body.apiVersion !== 'v1' ||
|
|
31
|
+
!Array.isArray(body.tools) ||
|
|
32
|
+
body.tools.length > 1024) {
|
|
33
|
+
throw new ManifestError('Unsupported tool manifest. Update the client or contact your administrator.');
|
|
34
|
+
}
|
|
35
|
+
const names = new Set();
|
|
36
|
+
const bindings = new Set();
|
|
37
|
+
return body.tools.map((value) => {
|
|
38
|
+
const tool = record(value);
|
|
39
|
+
if (typeof tool.name !== 'string' ||
|
|
40
|
+
!/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(tool.name) ||
|
|
41
|
+
!Number.isInteger(tool.version) ||
|
|
42
|
+
Number(tool.version) < 1 ||
|
|
43
|
+
!['read', 'write', 'destructive'].includes(String(tool.effect)) ||
|
|
44
|
+
!['none', 'preflight_confirm'].includes(String(tool.confirmation)) ||
|
|
45
|
+
typeof tool.description !== 'string' ||
|
|
46
|
+
!tool.inputSchema ||
|
|
47
|
+
typeof tool.inputSchema !== 'object' ||
|
|
48
|
+
Array.isArray(tool.inputSchema) ||
|
|
49
|
+
names.has(tool.name))
|
|
50
|
+
throw new ManifestError('Invalid or unsupported tool descriptor.');
|
|
51
|
+
names.add(tool.name);
|
|
52
|
+
checkSchema(tool.inputSchema);
|
|
53
|
+
if (tool.cli !== undefined) {
|
|
54
|
+
const binding = record(tool.cli);
|
|
55
|
+
const segment = (part) => typeof part === 'string' &&
|
|
56
|
+
part.length <= 64 &&
|
|
57
|
+
/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(part);
|
|
58
|
+
if (!Array.isArray(binding.group) ||
|
|
59
|
+
binding.group.length < 1 ||
|
|
60
|
+
binding.group.length > 6 ||
|
|
61
|
+
!binding.group.every(segment) ||
|
|
62
|
+
!segment(binding.operation))
|
|
63
|
+
throw new ManifestError('Invalid command binding.');
|
|
64
|
+
const key = JSON.stringify([binding.group, binding.operation]);
|
|
65
|
+
if (tool.version === 1 && bindings.has(key))
|
|
66
|
+
throw new ManifestError('Ambiguous command binding in tool manifest.');
|
|
67
|
+
if (tool.version === 1)
|
|
68
|
+
bindings.add(key);
|
|
69
|
+
}
|
|
70
|
+
return tool;
|
|
71
|
+
});
|
|
72
|
+
})();
|
|
73
|
+
cache.set(ctx, { baseUrl: ctx.baseUrl, token: ctx.token, manifest: loaded });
|
|
74
|
+
return loaded;
|
|
75
|
+
};
|
|
76
|
+
export const getTool = async (ctx, name) => {
|
|
77
|
+
const tool = (await getManifest(ctx)).find((candidate) => candidate.name === name);
|
|
78
|
+
if (!tool)
|
|
79
|
+
throw new ManifestError('This operation is not available in the current tool manifest.');
|
|
80
|
+
if (tool.version !== 1)
|
|
81
|
+
throw new ManifestError('This operation version is not supported. Update the CLI.');
|
|
82
|
+
if (tool.effect === 'destructive')
|
|
83
|
+
throw new ManifestError('This operation is not available through tools.');
|
|
84
|
+
return tool;
|
|
85
|
+
};
|
|
86
|
+
export const resolveBinding = async (ctx, group, operation) => {
|
|
87
|
+
const matches = (await getManifest(ctx)).filter((candidate) => candidate.cli?.operation === operation &&
|
|
88
|
+
JSON.stringify(candidate.cli.group) === JSON.stringify(group));
|
|
89
|
+
const tool = matches.find((candidate) => candidate.version === 1) ?? matches[0];
|
|
90
|
+
if (!tool)
|
|
91
|
+
throw new ManifestError('This command is not supported by the current tool manifest. Update the server or check available capabilities.');
|
|
92
|
+
return getTool(ctx, tool.name);
|
|
93
|
+
};
|
package/dist/toolSurface.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createApiClient } from './api.js';
|
|
2
2
|
import { resolveApiBaseUrl } from './config.js';
|
|
3
3
|
import { createCredentialStore } from './credentials.js';
|
|
4
|
+
import { getTool } from './manifest.js';
|
|
4
5
|
export const asRecord = (value) => value && typeof value === 'object' ? value : {};
|
|
5
6
|
export const asText = (value, fallback = '') => typeof value === 'string' ? value : fallback;
|
|
6
7
|
/**
|
|
@@ -35,7 +36,15 @@ export const resolveToolContext = async (apiUrl, store) => {
|
|
|
35
36
|
return { baseUrl, client: createApiClient(baseUrl), token: credential.token };
|
|
36
37
|
};
|
|
37
38
|
export const callTool = async (ctx, toolName, input, options = {}) => {
|
|
38
|
-
const
|
|
39
|
+
const tool = await getTool(ctx, toolName);
|
|
40
|
+
if (tool.effect !== 'read' && !options.idempotencyKey) {
|
|
41
|
+
throw new Error(`Refusing to call the write tool '${toolName}' without an idempotency key. Use applyTool.`);
|
|
42
|
+
}
|
|
43
|
+
if (options.idempotencyKey !== undefined &&
|
|
44
|
+
(!options.idempotencyKey.trim() || options.idempotencyKey.length > 256)) {
|
|
45
|
+
throw new Error('The idempotency key must contain 1 to 256 characters.');
|
|
46
|
+
}
|
|
47
|
+
const body = { input, version: tool.version };
|
|
39
48
|
if (options.confirmationToken)
|
|
40
49
|
body.confirmationToken = options.confirmationToken;
|
|
41
50
|
if (options.idempotencyKey)
|
package/dist/upload.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import { type ToolDescriptor } from './manifest.js';
|
|
1
2
|
import { type ToolContext } from './toolSurface.js';
|
|
2
|
-
/** Maximum evidence file size read and staged by the CLI (
|
|
3
|
+
/** Maximum evidence file size read and staged by the CLI (25 MiB, matching the tool surface). */
|
|
3
4
|
export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
4
5
|
export interface UploadLimits {
|
|
5
6
|
maxUploadBytes?: number;
|
|
6
7
|
timeoutMs?: number;
|
|
7
8
|
maxResponseBytes?: number;
|
|
8
9
|
}
|
|
10
|
+
export declare class FileInputError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export declare const readUploadFile: (filePath: string, maxBytes: number) => Promise<Buffer>;
|
|
9
13
|
/**
|
|
10
14
|
* Stages a local file through the `upload_file` tool and returns its upload id, ready to be attached
|
|
11
15
|
* to a target (a test run, a risk, a vendor). The file is sent as multipart to the tool surface with
|
|
12
16
|
* the bearer credential, so no cookie or CSRF token is involved.
|
|
13
17
|
*/
|
|
18
|
+
export declare const stageLocalFile: (ctx: ToolContext, filePath: string, purpose: string, limits?: UploadLimits, descriptor?: ToolDescriptor, idempotencyKey?: string) => Promise<{
|
|
19
|
+
id: string;
|
|
20
|
+
filename: string;
|
|
21
|
+
size: number;
|
|
22
|
+
envelope: Record<string, unknown>;
|
|
23
|
+
}>;
|
|
14
24
|
export declare const uploadLocalFile: (ctx: ToolContext, filePath: string, purpose: string, limits?: UploadLimits) => Promise<{
|
|
15
25
|
id: string;
|
|
16
26
|
filename: string;
|
package/dist/upload.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { getTool, ManifestError } from './manifest.js';
|
|
2
3
|
import { constants } from 'node:fs';
|
|
3
4
|
import { lstat, open } from 'node:fs/promises';
|
|
4
5
|
import { basename } from 'node:path';
|
|
@@ -19,15 +20,17 @@ const MIME_TYPES = {
|
|
|
19
20
|
'.xls': 'application/vnd.ms-excel',
|
|
20
21
|
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
21
22
|
};
|
|
22
|
-
/** Maximum evidence file size read and staged by the CLI (
|
|
23
|
-
export const DEFAULT_MAX_UPLOAD_BYTES =
|
|
24
|
-
|
|
23
|
+
/** Maximum evidence file size read and staged by the CLI (25 MiB, matching the tool surface). */
|
|
24
|
+
export const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
25
|
+
export class FileInputError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export const readUploadFile = async (filePath, maxBytes) => {
|
|
25
28
|
const initialStat = await lstat(filePath);
|
|
26
29
|
if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
|
|
27
|
-
throw new
|
|
30
|
+
throw new FileInputError('Upload path must be a regular file, not a symlink or special file.');
|
|
28
31
|
}
|
|
29
32
|
if (initialStat.size > maxBytes) {
|
|
30
|
-
throw new
|
|
33
|
+
throw new FileInputError(`Upload exceeds the ${maxBytes}-byte file size limit.`);
|
|
31
34
|
}
|
|
32
35
|
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
33
36
|
const file = await open(filePath, constants.O_RDONLY | noFollow);
|
|
@@ -36,24 +39,24 @@ const readUploadFile = async (filePath, maxBytes) => {
|
|
|
36
39
|
if (!openedStat.isFile() ||
|
|
37
40
|
openedStat.dev !== initialStat.dev ||
|
|
38
41
|
openedStat.ino !== initialStat.ino) {
|
|
39
|
-
throw new
|
|
42
|
+
throw new FileInputError('Upload path changed while it was being opened. Please retry.');
|
|
40
43
|
}
|
|
41
44
|
if (openedStat.size > maxBytes) {
|
|
42
|
-
throw new
|
|
45
|
+
throw new FileInputError(`Upload exceeds the ${maxBytes}-byte file size limit.`);
|
|
43
46
|
}
|
|
44
47
|
const chunks = [];
|
|
45
48
|
let total = 0;
|
|
46
49
|
while (true) {
|
|
47
50
|
const remaining = maxBytes + 1 - total;
|
|
48
51
|
if (remaining <= 0)
|
|
49
|
-
throw new
|
|
52
|
+
throw new FileInputError(`Upload exceeds the ${maxBytes}-byte file size limit.`);
|
|
50
53
|
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
|
|
51
54
|
const { bytesRead } = await file.read(chunk, 0, chunk.length, null);
|
|
52
55
|
if (bytesRead === 0)
|
|
53
56
|
break;
|
|
54
57
|
total += bytesRead;
|
|
55
58
|
if (total > maxBytes)
|
|
56
|
-
throw new
|
|
59
|
+
throw new FileInputError(`Upload exceeds the ${maxBytes}-byte file size limit.`);
|
|
57
60
|
chunks.push(chunk.subarray(0, bytesRead));
|
|
58
61
|
}
|
|
59
62
|
return Buffer.concat(chunks, total);
|
|
@@ -72,16 +75,26 @@ const contentTypeFor = (filename) => {
|
|
|
72
75
|
* to a target (a test run, a risk, a vendor). The file is sent as multipart to the tool surface with
|
|
73
76
|
* the bearer credential, so no cookie or CSRF token is involved.
|
|
74
77
|
*/
|
|
75
|
-
export const
|
|
78
|
+
export const stageLocalFile = async (ctx, filePath, purpose, limits = {}, descriptor, idempotencyKey = randomUUID()) => {
|
|
79
|
+
const tool = descriptor ?? (await getTool(ctx, 'upload_file'));
|
|
80
|
+
if (tool.effect !== 'write' || tool.confirmation !== 'none') {
|
|
81
|
+
throw new ManifestError('The server does not support immediate file staging.');
|
|
82
|
+
}
|
|
83
|
+
const properties = asRecord(tool.inputSchema.properties);
|
|
84
|
+
const purposes = asRecord(properties.purpose).enum;
|
|
85
|
+
if (!Array.isArray(purposes) || !purposes.includes(purpose))
|
|
86
|
+
throw new Error('Unsupported upload purpose.');
|
|
87
|
+
if (!idempotencyKey.trim() || idempotencyKey.length > 256)
|
|
88
|
+
throw new Error('The idempotency key must contain 1 to 256 characters.');
|
|
76
89
|
const filename = basename(filePath);
|
|
77
90
|
const contents = await readUploadFile(filePath, limits.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES);
|
|
78
91
|
const form = new FormData();
|
|
79
92
|
form.append('purpose', purpose);
|
|
80
93
|
form.append('file', new Blob([new Uint8Array(contents)], { type: contentTypeFor(filename) }), filename);
|
|
81
94
|
return withRequestTimeout(limits.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, async (signal) => {
|
|
82
|
-
const response = await fetchWithoutRedirects(`${ctx.baseUrl}/tools/v1
|
|
95
|
+
const response = await fetchWithoutRedirects(`${ctx.baseUrl}/tools/v1/${encodeURIComponent(tool.name)}`, {
|
|
83
96
|
method: 'POST',
|
|
84
|
-
headers: { authorization: `Bearer ${ctx.token}`, 'idempotency-key':
|
|
97
|
+
headers: { authorization: `Bearer ${ctx.token}`, 'idempotency-key': idempotencyKey },
|
|
85
98
|
body: form,
|
|
86
99
|
signal,
|
|
87
100
|
});
|
|
@@ -100,7 +113,9 @@ export const uploadLocalFile = async (ctx, filePath, purpose, limits = {}) => {
|
|
|
100
113
|
const message = typeof asRecord(envelope.error).message === 'string'
|
|
101
114
|
? asRecord(envelope.error).message
|
|
102
115
|
: `Upload failed with status ${response.status}`;
|
|
103
|
-
throw new ApiError(message, response.status)
|
|
116
|
+
throw new ApiError(message, response.status, typeof asRecord(envelope.error).code === 'string'
|
|
117
|
+
? String(asRecord(envelope.error).code)
|
|
118
|
+
: undefined, asRecord(envelope.error).details, envelope);
|
|
104
119
|
}
|
|
105
120
|
const payload = asRecord(envelope.data);
|
|
106
121
|
const id = typeof payload.uploadId === 'string' ? payload.uploadId : null;
|
|
@@ -108,8 +123,13 @@ export const uploadLocalFile = async (ctx, filePath, purpose, limits = {}) => {
|
|
|
108
123
|
throw new ApiError('Upload did not return an id', 502);
|
|
109
124
|
return {
|
|
110
125
|
id,
|
|
126
|
+
envelope,
|
|
111
127
|
filename: typeof payload.filename === 'string' ? payload.filename : filename,
|
|
112
128
|
size: typeof payload.size === 'number' ? payload.size : contents.length,
|
|
113
129
|
};
|
|
114
130
|
});
|
|
115
131
|
};
|
|
132
|
+
export const uploadLocalFile = async (ctx, filePath, purpose, limits = {}) => {
|
|
133
|
+
const { id, filename, size } = await stageLocalFile(ctx, filePath, purpose, limits);
|
|
134
|
+
return { id, filename, size };
|
|
135
|
+
};
|
package/package.json
CHANGED
package/skills/ciphrix/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ciphrix
|
|
3
|
-
description: Operate a Ciphrix compliance programme with the `ciphrix` CLI. Use
|
|
3
|
+
description: Operate a Ciphrix compliance programme with the `ciphrix` CLI. Use for onboarding and organizational context, documents, frameworks, controls, tests and evidence, access reviews and imports, agent findings, assigned tasks, risks, vendors, assets, monitoring checks, or background jobs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Ciphrix
|
|
@@ -140,6 +140,31 @@ are what a check raised. `check disable` silences a check for the tenant — rec
|
|
|
140
140
|
|
|
141
141
|
## Guardrails
|
|
142
142
|
|
|
143
|
+
**Context and onboarding.** Read the server status and question schema, then save only facts agreed
|
|
144
|
+
with the user. Business and Operating Context are separate. Let the server decide readiness; initial
|
|
145
|
+
setup is optional and journey completion is explicit. A context save never authorizes starting agents,
|
|
146
|
+
initializing a framework or completing onboarding. Replayed packs are snapshots: read status again for
|
|
147
|
+
current readiness.
|
|
148
|
+
|
|
149
|
+
**Access reviews and proposals.** Inventory is not a review decision. Inspect proposal rows, matches,
|
|
150
|
+
source evidence and current revisions before selecting a merge. Never manufacture person-match or
|
|
151
|
+
visual-evidence confirmation. Keep/revoke decisions, reviewer-task completion and campaign ending are
|
|
152
|
+
distinct human intents. A revoke decision is a removal request, not provider-side revocation. Agent
|
|
153
|
+
findings/options and AI assessments are advisory; inspect the complete current proposal and exact
|
|
154
|
+
declared option before any explicit decision. Assigned-task discovery grants no completion authority.
|
|
155
|
+
|
|
156
|
+
**Writes and uncertainty.** Discover capabilities through the server, use the exact reviewed inputs,
|
|
157
|
+
and treat confirmation as explicit delegated consent, not an obstacle to automate around. Retain one
|
|
158
|
+
idempotency key per logical operation; never retry unknown outcomes with a new key. The CLI performs no
|
|
159
|
+
automatic write retries and does not promise confirmed recovery across invocations or expired tokens.
|
|
160
|
+
Inspect state/jobs when uncertain. Batch results can partially apply; preserve and report failures.
|
|
161
|
+
Advisory assessments never authorize a formal passing result.
|
|
162
|
+
|
|
163
|
+
**Files.** Stage and process only the files the user selected. General processing may update context
|
|
164
|
+
or create vendor records; staging alone does not authorize that processing. Imports are proposed work,
|
|
165
|
+
not merged inventory. Temporary evidence/export references expire; do not forward credentials to them,
|
|
166
|
+
claim bytes were downloaded when only a reference was returned, or treat a replayed URL as renewed.
|
|
167
|
+
|
|
143
168
|
- Work only in the tenant and API environment the authenticated user selected. Never discover or switch
|
|
144
169
|
tenants, endpoints or credential stores by guessing.
|
|
145
170
|
- Do not expose access tokens, stored credentials or sensitive response data in prose, logs or commands.
|