@ciphrix/cli 0.1.1 → 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/README.md +81 -5
- 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 +20 -5
- package/dist/commands/capabilities.d.ts +6 -0
- package/dist/commands/capabilities.js +311 -0
- package/dist/commands/capabilityDefinitions.d.ts +18 -0
- package/dist/commands/capabilityDefinitions.js +244 -0
- package/dist/commands/control.d.ts +2 -1
- package/dist/commands/control.js +11 -1
- package/dist/commands/framework.js +2 -1
- package/dist/commands/policyActions.js +12 -2
- package/dist/commands/test.d.ts +2 -2
- package/dist/commands/test.js +18 -3
- 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.d.ts +0 -8
- package/dist/toolSurface.js +8 -44
- package/dist/upload.d.ts +10 -0
- package/dist/upload.js +31 -11
- package/package.json +1 -1
- package/skills/ciphrix/SKILL.md +42 -3
- package/skills/ciphrix/references/commands.md +1656 -7
- package/skills/ciphrix/references/document-workflows.md +62 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const help = {
|
|
2
|
+
page: 'page number starting at 1',
|
|
3
|
+
limit: 'page size; server limits apply',
|
|
4
|
+
search: 'search visible records',
|
|
5
|
+
'sort-by': 'server-supported sort field',
|
|
6
|
+
'sort-order': 'sort direction: asc or desc',
|
|
7
|
+
status: 'inventory status: active or inactive; review/agent operations use their own status vocabulary',
|
|
8
|
+
name: 'record name',
|
|
9
|
+
website: 'resource website',
|
|
10
|
+
'owner-person': 'existing People ID for the resource owner',
|
|
11
|
+
'include-empty': 'include resources with no access entries',
|
|
12
|
+
'expected-updated-at': 'exact updatedAt timestamp from the latest read',
|
|
13
|
+
'clear-website': 'clear the website; conflicts with --website',
|
|
14
|
+
'clear-owner': 'clear the owner; conflicts with --owner-person',
|
|
15
|
+
resource: 'existing resource ID; repeat for multiple selections where supported',
|
|
16
|
+
person: 'existing People ID for the access identity',
|
|
17
|
+
'review-state': 'inventory: needs_review, pending, keep, revoke or inactive; reviewer task: pending or all',
|
|
18
|
+
'reviewed-before': 'ISO review-history cutoff',
|
|
19
|
+
identity: 'reported account identity; does not change authentication',
|
|
20
|
+
role: 'inventory access role; does not change product authorization',
|
|
21
|
+
'clear-person': 'leave the identity unmatched; conflicts with --person',
|
|
22
|
+
'expected-revision': 'current revision from the latest entry or reviewer-task read',
|
|
23
|
+
'rows-file': 'JSON array of explicitly selected rows; regular file, at most 1 MiB',
|
|
24
|
+
field: 'single inventory field to update on every selected row',
|
|
25
|
+
value: 'strict JSON value; quote strings as JSON and use null only where supported',
|
|
26
|
+
action: 'explicit link or unlink action',
|
|
27
|
+
upload: 'ready upload ID owned by the caller; repeat to select multiple files',
|
|
28
|
+
cursor: 'continuation cursor returned by the preceding page',
|
|
29
|
+
filter: 'proposal-row filter: all, new, unchanged, update or conflict',
|
|
30
|
+
'ids-only': 'return proposal-row IDs instead of full details',
|
|
31
|
+
'due-at': 'ISO due date/time; review creation defaults to 14 days if omitted',
|
|
32
|
+
'items-file': 'JSON array of accessId, expectedRevision, decision and optional comment; at most 1 MiB',
|
|
33
|
+
outcome: 'frozen outcome filter: keep, revoke, pending, unresolved or inactive',
|
|
34
|
+
agent: 'agent filter; repeat for multiple values',
|
|
35
|
+
'action-type': 'action-type filter; repeat for multiple values',
|
|
36
|
+
'initiator-type': 'initiator-type filter; repeat for multiple values',
|
|
37
|
+
from: 'ISO lower date/time bound',
|
|
38
|
+
to: 'ISO upper date/time bound',
|
|
39
|
+
offset: 'number of grouped results to skip',
|
|
40
|
+
job: 'existing background job ID',
|
|
41
|
+
'review-only': 'read the current human-review queue',
|
|
42
|
+
decision: 'explicit human decision: approve, reject or choose',
|
|
43
|
+
option: 'exact declared option for choose only; no delete options',
|
|
44
|
+
reason: 'human explanation for the decision',
|
|
45
|
+
state: 'active or history',
|
|
46
|
+
'task-type': 'active-task filter: all, todo, assignment or action; omit with history',
|
|
47
|
+
purpose: 'staging purpose advertised by the server; does not process the file',
|
|
48
|
+
attachment: 'evidence attachment ID belonging to the selected test run',
|
|
49
|
+
run: 'existing run ID; omit to use the current run',
|
|
50
|
+
};
|
|
51
|
+
const field = (flag, target = flag.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()), kind = 'text', required = false, description = help[flag] ?? `value for ${flag}`) => ({ flag, field: target, kind, required, help: description });
|
|
52
|
+
const page = () => [field('page', 'page', 'integer'), field('limit', 'limit', 'integer')];
|
|
53
|
+
const sort = () => [field('sort-by'), field('sort-order')];
|
|
54
|
+
const spec = (path, description, argument, fields, next) => ({ path, description, ...(argument && { argument }), fields, next });
|
|
55
|
+
const filters = () => [
|
|
56
|
+
field('agent', 'agent', 'repeat'),
|
|
57
|
+
field('action-type', 'actionType', 'repeat'),
|
|
58
|
+
field('status', 'status', 'repeat', false, 'status filter; repeat for multiple values'),
|
|
59
|
+
field('initiator-type', 'initiatorType', 'repeat'),
|
|
60
|
+
field('from'),
|
|
61
|
+
field('to'),
|
|
62
|
+
field('search', 'q'),
|
|
63
|
+
field('limit', 'limit', 'integer'),
|
|
64
|
+
];
|
|
65
|
+
export const capabilities = [
|
|
66
|
+
...[
|
|
67
|
+
['document workflow set', 'Set workflow type in draft.', 'workflowType'],
|
|
68
|
+
['document reviewer set', 'Set reviewer; sent requests must be recalled first.', 'user'],
|
|
69
|
+
['document reviewer remove', 'Remove the reviewer from a draft.'],
|
|
70
|
+
['document approver set', 'Set approver; sent requests must be recalled first.', 'user'],
|
|
71
|
+
['document approver remove', 'Remove approver; sent requests must be recalled first.'],
|
|
72
|
+
[
|
|
73
|
+
'document review complete',
|
|
74
|
+
'Record an explicitly authorized review decision; does not send approval.',
|
|
75
|
+
],
|
|
76
|
+
['document review send-back', 'Record an explicit negative review decision; clears reviewer.'],
|
|
77
|
+
[
|
|
78
|
+
'document review cancel',
|
|
79
|
+
'Recall review request; clears reviewer. Configure and resend separately.',
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
'document approval send',
|
|
83
|
+
'Send for approval after review; never completes review implicitly.',
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'document approval send-back',
|
|
87
|
+
'Record an explicit negative approval decision; clears approver.',
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'document approval cancel',
|
|
91
|
+
'Recall approval request; clears approver. Configure and resend separately.',
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
'document approval self-approve',
|
|
95
|
+
'Explicit self-approval, separate from ordinary approve; no review bypass.',
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
'document workflow return-to-draft',
|
|
99
|
+
'Return eligible advanced approval-stage document to draft; clears assignments.',
|
|
100
|
+
],
|
|
101
|
+
].map(([path, description, input]) => ({
|
|
102
|
+
path: path,
|
|
103
|
+
description: description,
|
|
104
|
+
argument: 'document',
|
|
105
|
+
inputAdapter: 'documentWorkflow',
|
|
106
|
+
fields: input === 'workflowType'
|
|
107
|
+
? [field('type', 'workflowType', 'text', true, 'workflow type: simple or advanced')]
|
|
108
|
+
: input === 'user'
|
|
109
|
+
? [
|
|
110
|
+
field('user', 'user', 'text', true, 'exact active user email or User ID; employees may be assigned'),
|
|
111
|
+
]
|
|
112
|
+
: [],
|
|
113
|
+
next: 'document get',
|
|
114
|
+
})),
|
|
115
|
+
spec('user list', 'List active tenant users, including employees; discovery does not change accounts.', undefined, [
|
|
116
|
+
field('search', 'search', 'text', false, 'optional name/email search, at least three characters'),
|
|
117
|
+
...page(),
|
|
118
|
+
], 'document reviewer set'),
|
|
119
|
+
spec('access resource list', 'List review inventory resources and owners.', undefined, [
|
|
120
|
+
...page(),
|
|
121
|
+
field('search'),
|
|
122
|
+
...sort(),
|
|
123
|
+
field('status'),
|
|
124
|
+
field('owner-person', 'ownerPersonId'),
|
|
125
|
+
field('include-empty', 'includeEmpty', 'boolean'),
|
|
126
|
+
], 'access resource get'),
|
|
127
|
+
spec('access resource get', 'Read a resource and its current inventory context.', 'resourceId', [], 'access entry list'),
|
|
128
|
+
spec('access resource create', 'Create manual inventory; reconcile active reviews.', undefined, [field('name', 'name', 'text', true), field('website'), field('owner-person', 'ownerPersonId')], 'access resource get'),
|
|
129
|
+
spec('access resource update', 'Update inventory. Inactivation also deactivates child access.', 'resourceId', [
|
|
130
|
+
field('expected-updated-at'),
|
|
131
|
+
field('name'),
|
|
132
|
+
field('website'),
|
|
133
|
+
field('clear-website', 'website', 'clear'),
|
|
134
|
+
field('owner-person', 'ownerPersonId'),
|
|
135
|
+
field('clear-owner', 'ownerPersonId', 'clear'),
|
|
136
|
+
field('status'),
|
|
137
|
+
], 'access resource get'),
|
|
138
|
+
spec('access resource link-asset', 'Link or unlink an existing asset; reconcile reviews.', 'resourceId assetId', [field('action', 'action', 'text', true)], 'access resource get'),
|
|
139
|
+
spec('access resource bulk-update', 'Update selected resource owners/statuses; inspect per-row failures.', undefined, [
|
|
140
|
+
field('resource', 'resourceIds', 'repeat', true),
|
|
141
|
+
field('owner-person', 'ownerPersonId'),
|
|
142
|
+
field('clear-owner', 'ownerPersonId', 'clear'),
|
|
143
|
+
field('status'),
|
|
144
|
+
], 'access resource list'),
|
|
145
|
+
spec('access entry list', 'List current access inventory; no review decisions are recorded.', undefined, [
|
|
146
|
+
...page(),
|
|
147
|
+
field('search'),
|
|
148
|
+
...sort(),
|
|
149
|
+
field('resource', 'resourceId'),
|
|
150
|
+
field('person', 'personId'),
|
|
151
|
+
field('owner-person', 'ownerPersonId'),
|
|
152
|
+
field('status'),
|
|
153
|
+
field('review-state'),
|
|
154
|
+
field('reviewed-before'),
|
|
155
|
+
], 'access entry get'),
|
|
156
|
+
spec('access entry get', 'Read current inventory revision and prior review outcome.', 'accessId', [], 'access entry update'),
|
|
157
|
+
spec('access entry create', 'Create manual access inventory, not a review decision.', undefined, [
|
|
158
|
+
field('resource', 'resourceId', 'text', true),
|
|
159
|
+
field('identity', 'identity', 'text', true),
|
|
160
|
+
field('role', 'role', 'text', true),
|
|
161
|
+
field('person', 'personId'),
|
|
162
|
+
field('status'),
|
|
163
|
+
], 'access entry get'),
|
|
164
|
+
spec('access entry update', 'Update revisioned inventory; integration-owned fields stay locked.', 'accessId', [
|
|
165
|
+
field('expected-revision', 'expectedRevision', 'integer', true),
|
|
166
|
+
field('identity'),
|
|
167
|
+
field('role'),
|
|
168
|
+
field('person', 'personId'),
|
|
169
|
+
field('clear-person', 'personId', 'clear'),
|
|
170
|
+
field('status'),
|
|
171
|
+
], 'access entry get'),
|
|
172
|
+
spec('access entry bulk-update', 'Update one field on selected revisioned entries; inspect partial failures.', undefined, [
|
|
173
|
+
field('rows-file', 'rows', 'file', true),
|
|
174
|
+
field('field', 'field', 'text', true),
|
|
175
|
+
field('value', 'value', 'json', true),
|
|
176
|
+
], 'access entry list'),
|
|
177
|
+
spec('access import start', 'Extract a proposal from owned register_import uploads; does not merge.', undefined, [field('upload', 'uploadIds', 'repeat', true)], 'access import get'),
|
|
178
|
+
spec('access import list', 'List your unresolved imports using cursor pagination.', undefined, [field('cursor'), field('limit', 'limit', 'integer')], 'access import get'),
|
|
179
|
+
spec('access import get', 'Inspect proposal rows, matches, evidence and revisions before merging.', 'importId', [
|
|
180
|
+
field('cursor'),
|
|
181
|
+
field('limit', 'limit', 'integer'),
|
|
182
|
+
field('filter'),
|
|
183
|
+
field('search'),
|
|
184
|
+
field('ids-only', 'idsOnly', 'boolean'),
|
|
185
|
+
], 'access import merge'),
|
|
186
|
+
spec('access import merge', 'Merge only reviewed selected rows; preserve corrections and explicit confirmations.', 'importId', [field('rows-file', 'rows', 'file', true)], 'access import get'),
|
|
187
|
+
spec('access import discard', 'Stop extraction/merge; retain successful merges and independent enrichment.', 'importId', [], 'access import list'),
|
|
188
|
+
spec('access review preview', 'Preview selected eligible owners and unresolved entries without creating work.', undefined, [field('resource', 'resourceIds', 'repeat', true), field('due-at')], 'access review start'),
|
|
189
|
+
spec('access review start', 'Create a review, reviewer tasks and assignment notifications.', undefined, [
|
|
190
|
+
field('name', 'name', 'text', true),
|
|
191
|
+
field('resource', 'resourceIds', 'repeat', true),
|
|
192
|
+
field('due-at'),
|
|
193
|
+
], 'access review list'),
|
|
194
|
+
spec('access review list', 'Read active progress or closed/cancelled history.', undefined, [
|
|
195
|
+
field('status', 'status', 'text', false, 'review filter: active, closed, cancelled or history'),
|
|
196
|
+
...page(),
|
|
197
|
+
], 'access review task'),
|
|
198
|
+
spec('access review task', 'Read your assigned reviewer task, current revisions and decisions.', 'taskId', [...page(), field('search'), field('review-state')], 'access review decide'),
|
|
199
|
+
spec('access review decide', 'Record explicit keep/revoke decisions. Revoke is a removal request, not provider revocation.', 'taskId', [field('items-file', 'items', 'file', true)], 'access review task'),
|
|
200
|
+
spec('access review complete', 'Complete your assigned task after all decisions; may close a fully reviewed campaign.', 'taskId', [], 'access review list'),
|
|
201
|
+
spec('access review result', 'Read frozen outcomes; pending/unresolved never imply approval.', 'reviewId', [...page(), field('search'), field('resource', 'resourceId'), field('outcome')], 'access review export'),
|
|
202
|
+
spec('access review end', 'Explicitly close with current outcomes or cancel without a result; close reviewer tasks.', 'reviewId', [
|
|
203
|
+
field('status', 'status', 'text', true, 'closed freezes outcomes; cancelled creates no result'),
|
|
204
|
+
], 'access review result'),
|
|
205
|
+
spec('access review export', 'Create a temporary signed CSV reference; no automatic download.', 'reviewId', [], 'access review result'),
|
|
206
|
+
spec('agent run list', 'List grouped agent activity; counts are not completed assessments.', undefined, [...filters(), field('offset', 'offset', 'integer')], 'agent action list'),
|
|
207
|
+
spec('agent action list', 'Inspect agent findings or current review work; preserve truncation and cursors.', undefined, [
|
|
208
|
+
...filters(),
|
|
209
|
+
field('job', 'aiTaskId'),
|
|
210
|
+
field('cursor'),
|
|
211
|
+
field('review-only', 'reviewOnly', 'boolean'),
|
|
212
|
+
], 'agent action get'),
|
|
213
|
+
spec('agent action get', 'Read a proposal and its declared options before deciding.', 'agentActionId', [], 'agent action decide'),
|
|
214
|
+
spec('agent action decide', 'Apply an explicit reviewed decision; unsupported, delete and truncated approvals are refused.', 'agentActionId', [
|
|
215
|
+
field('decision', 'decision', 'text', true),
|
|
216
|
+
field('expected-updated-at', 'expectedUpdatedAt', 'text', true),
|
|
217
|
+
field('option'),
|
|
218
|
+
field('reason'),
|
|
219
|
+
], 'agent action get'),
|
|
220
|
+
spec('task list', 'Read your assigned stored tasks; does not refresh assignments or complete work.', undefined, [field('state'), field('task-type'), ...page()], 'task get'),
|
|
221
|
+
spec('task get', 'Read your task and its supported next capability; unsupported actions remain in the app.', 'taskId', [], 'access review task'),
|
|
222
|
+
spec('file stage', 'Stage a local regular file as your current identity; at most 25 MiB, no automatic processing.', 'path', [field('purpose', 'purpose', 'text', true)], 'file process'),
|
|
223
|
+
spec('file process', 'Process owned documents-purpose uploads. May update context or create vendors; starts background work.', undefined, [field('upload', 'uploadIds', 'repeat', true)], 'job status'),
|
|
224
|
+
spec('test run file', 'Create a temporary reference for evidence linked to this test/run; no automatic download.', 'name', [field('attachment', 'attachmentId', 'text', true), field('run', 'runId')], 'test items'),
|
|
225
|
+
spec('test run assess', 'Request advisory AI assessment; does not change the human run result.', 'name', [field('run', 'runId')], 'test get'),
|
|
226
|
+
spec('control assess', 'Request/reuse advisory assessment. A cached result may have no job ID.', 'name', [], 'control assessment'),
|
|
227
|
+
spec('control assessment', 'Read advisory assessment/progress; absence means not assessed, not compliant.', 'name', [], 'control get'),
|
|
228
|
+
];
|
|
229
|
+
export const onboardingOperations = [
|
|
230
|
+
['status', 'read current journey and separate context readiness'],
|
|
231
|
+
['help-onboarding', 'read authenticated server help, all options and current status'],
|
|
232
|
+
['get-context', 'read business/operating schema and answers'],
|
|
233
|
+
['set-context', 'save one agreed business/operating answer without completing the journey'],
|
|
234
|
+
[
|
|
235
|
+
'run-vendor-agent',
|
|
236
|
+
'start vendor discovery; may create vendors; requires Business Context 100%',
|
|
237
|
+
],
|
|
238
|
+
['run-risk-agent', 'start initial risk generation; requires Business Context 100%'],
|
|
239
|
+
[
|
|
240
|
+
'init-framework',
|
|
241
|
+
'initialize a selected framework; may consume quota and notify administrators',
|
|
242
|
+
],
|
|
243
|
+
['complete', 'explicitly complete with Business Context 100%; operating/setup remain optional'],
|
|
244
|
+
];
|
|
@@ -9,8 +9,9 @@ export interface ControlCommonOptions {
|
|
|
9
9
|
export interface ControlReadOptions extends ControlCommonOptions {
|
|
10
10
|
name: string;
|
|
11
11
|
}
|
|
12
|
-
export declare const controlListCommand: ({ io, apiUrl, json, credentialStore, search, domain, status, applicability, ownerId, page, limit, }: ControlCommonOptions & {
|
|
12
|
+
export declare const controlListCommand: ({ io, apiUrl, json, credentialStore, search, framework, domain, status, applicability, ownerId, page, limit, }: ControlCommonOptions & {
|
|
13
13
|
search?: string | undefined;
|
|
14
|
+
framework?: string[] | undefined;
|
|
14
15
|
domain?: string | undefined;
|
|
15
16
|
status?: string | undefined;
|
|
16
17
|
applicability?: string | undefined;
|
package/dist/commands/control.js
CHANGED
|
@@ -2,11 +2,13 @@ import { applyTool } from '../actions.js';
|
|
|
2
2
|
import { writeLine, writeNextHint, writePageFooter, writeTable } from '../io.js';
|
|
3
3
|
import { asRecord, asText, callTool, errorMessage, resolveToolContext } from '../toolSurface.js';
|
|
4
4
|
const text = (value, fallback = '-') => typeof value === 'string' ? value : typeof value === 'number' ? String(value) : fallback;
|
|
5
|
-
export const controlListCommand = async ({ io, apiUrl, json = false, credentialStore, search, domain, status, applicability, ownerId, page, limit, }) => {
|
|
5
|
+
export const controlListCommand = async ({ io, apiUrl, json = false, credentialStore, search, framework, domain, status, applicability, ownerId, page, limit, }) => {
|
|
6
6
|
const ctx = await resolveToolContext(apiUrl, credentialStore);
|
|
7
7
|
const input = {};
|
|
8
8
|
if (search)
|
|
9
9
|
input.search = search;
|
|
10
|
+
if (framework !== undefined)
|
|
11
|
+
input.framework = framework;
|
|
10
12
|
if (domain)
|
|
11
13
|
input.domain = [domain];
|
|
12
14
|
if (status)
|
|
@@ -86,6 +88,14 @@ export const controlGetCommand = async ({ io, name, apiUrl, json = false, creden
|
|
|
86
88
|
continue;
|
|
87
89
|
writeLine(io.stdout, ` ${io.theme.dim(`${label}:`)} ${text(value)}`);
|
|
88
90
|
}
|
|
91
|
+
if (Array.isArray(control.complianceMap) && control.complianceMap.length > 0) {
|
|
92
|
+
writeLine(io.stdout, 'Catalogue framework relationships (not a compliance assessment):');
|
|
93
|
+
writeLine(io.stdout, JSON.stringify(control.complianceMap, null, 2));
|
|
94
|
+
}
|
|
95
|
+
if (control.items !== undefined) {
|
|
96
|
+
writeLine(io.stdout, 'Linked items:');
|
|
97
|
+
writeLine(io.stdout, JSON.stringify(control.items, null, 2));
|
|
98
|
+
}
|
|
89
99
|
if (typeof control.notes === 'string' && control.notes.trim() !== '') {
|
|
90
100
|
writeLine(io.stdout);
|
|
91
101
|
writeLine(io.stdout, control.notes);
|
|
@@ -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
|
};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { applyTool } from '../actions.js';
|
|
3
|
+
import { getManifest, resolveBinding } from '../manifest.js';
|
|
3
4
|
import { writeLine, writeTable } from '../io.js';
|
|
4
5
|
import { asRecord, asText, callTool, errorMessage, resolveToolContext } from '../toolSurface.js';
|
|
5
6
|
const UUID = /^[0-9a-f]{8}-[0-9a-f-]{27}$/i;
|
|
6
7
|
const num = (value, fallback) => typeof value === 'number' ? String(value) : asText(value, fallback);
|
|
7
8
|
const target = (policy) => UUID.test(policy) ? { policyId: policy } : { name: policy };
|
|
9
|
+
// Retain the original named calls only for older manifests without these bindings.
|
|
10
|
+
const documentWriteName = async (ctx, operation) => {
|
|
11
|
+
const manifest = await getManifest(ctx);
|
|
12
|
+
if (manifest.some((tool) => tool.cli?.operation === operation &&
|
|
13
|
+
JSON.stringify(tool.cli.group) === JSON.stringify(['document']))) {
|
|
14
|
+
return (await resolveBinding(ctx, ['document'], operation)).name;
|
|
15
|
+
}
|
|
16
|
+
return operation === 'submit' ? 'submit_document' : 'approve_document';
|
|
17
|
+
};
|
|
8
18
|
const renderResult = (io, json, result) => {
|
|
9
19
|
if (json) {
|
|
10
20
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
@@ -77,7 +87,7 @@ export const policySubmitCommand = async (options) => {
|
|
|
77
87
|
const result = await applyTool({
|
|
78
88
|
ctx,
|
|
79
89
|
io,
|
|
80
|
-
toolName: '
|
|
90
|
+
toolName: await documentWriteName(ctx, 'submit'),
|
|
81
91
|
input: target(policy),
|
|
82
92
|
fallback: 'Could not submit the document for review.',
|
|
83
93
|
yes,
|
|
@@ -92,7 +102,7 @@ export const policyApproveCommand = async (options) => {
|
|
|
92
102
|
const result = await applyTool({
|
|
93
103
|
ctx,
|
|
94
104
|
io,
|
|
95
|
-
toolName: '
|
|
105
|
+
toolName: await documentWriteName(ctx, 'approve'),
|
|
96
106
|
input: target(policy),
|
|
97
107
|
fallback: 'Could not approve the document.',
|
|
98
108
|
yes,
|
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 {
|
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)
|
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.d.ts
CHANGED
|
@@ -17,12 +17,4 @@ export interface CallToolOptions {
|
|
|
17
17
|
confirmationToken?: string | undefined;
|
|
18
18
|
idempotencyKey?: string | undefined;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
* The tool surface requires an idempotency key on every write and rejects a write without one. Writes
|
|
22
|
-
* must go through `applyTool`, which supplies the key and handles the preflight/confirmation contract.
|
|
23
|
-
* This set is the CLI-side mirror of the tool surface's write effects, so a write cannot be issued as a
|
|
24
|
-
* bare read-style call again. Keep it in step with the write tools the CLI exposes (ideally derived from
|
|
25
|
-
* the manifest once the CLI consumes it).
|
|
26
|
-
*/
|
|
27
|
-
export declare const WRITE_TOOL_NAMES: ReadonlySet<string>;
|
|
28
20
|
export declare const callTool: (ctx: ToolContext, toolName: string, input: unknown, options?: CallToolOptions) => Promise<Record<string, unknown>>;
|
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
|
/**
|
|
@@ -34,53 +35,16 @@ export const resolveToolContext = async (apiUrl, store) => {
|
|
|
34
35
|
throw new Error('Not signed in. Run `ciphrix login` first.');
|
|
35
36
|
return { baseUrl, client: createApiClient(baseUrl), token: credential.token };
|
|
36
37
|
};
|
|
37
|
-
/**
|
|
38
|
-
* The tool surface requires an idempotency key on every write and rejects a write without one. Writes
|
|
39
|
-
* must go through `applyTool`, which supplies the key and handles the preflight/confirmation contract.
|
|
40
|
-
* This set is the CLI-side mirror of the tool surface's write effects, so a write cannot be issued as a
|
|
41
|
-
* bare read-style call again. Keep it in step with the write tools the CLI exposes (ideally derived from
|
|
42
|
-
* the manifest once the CLI consumes it).
|
|
43
|
-
*/
|
|
44
|
-
export const WRITE_TOOL_NAMES = new Set([
|
|
45
|
-
'set_context_answer',
|
|
46
|
-
'reset_context',
|
|
47
|
-
'create_document',
|
|
48
|
-
'edit_document_content',
|
|
49
|
-
'create_document_version',
|
|
50
|
-
'update_document',
|
|
51
|
-
'submit_document',
|
|
52
|
-
'approve_document',
|
|
53
|
-
'delete_document',
|
|
54
|
-
'create_test_run',
|
|
55
|
-
'set_test_run_status',
|
|
56
|
-
'delete_test_run',
|
|
57
|
-
'attach_run_item',
|
|
58
|
-
'update_test',
|
|
59
|
-
'delete_test',
|
|
60
|
-
'update_clause',
|
|
61
|
-
'link_clause_item',
|
|
62
|
-
'update_control',
|
|
63
|
-
'link_control_item',
|
|
64
|
-
'create_vendor',
|
|
65
|
-
'update_vendor',
|
|
66
|
-
'delete_vendor',
|
|
67
|
-
'link_vendor_file',
|
|
68
|
-
'create_risk',
|
|
69
|
-
'update_risk',
|
|
70
|
-
'delete_risk',
|
|
71
|
-
'link_risk_control',
|
|
72
|
-
'link_risk_file',
|
|
73
|
-
'set_check_enabled',
|
|
74
|
-
'create_asset',
|
|
75
|
-
'update_asset',
|
|
76
|
-
'delete_asset',
|
|
77
|
-
'set_asset_tag',
|
|
78
|
-
]);
|
|
79
38
|
export const callTool = async (ctx, toolName, input, options = {}) => {
|
|
80
|
-
|
|
39
|
+
const tool = await getTool(ctx, toolName);
|
|
40
|
+
if (tool.effect !== 'read' && !options.idempotencyKey) {
|
|
81
41
|
throw new Error(`Refusing to call the write tool '${toolName}' without an idempotency key. Use applyTool.`);
|
|
82
42
|
}
|
|
83
|
-
|
|
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 };
|
|
84
48
|
if (options.confirmationToken)
|
|
85
49
|
body.confirmationToken = options.confirmationToken;
|
|
86
50
|
if (options.idempotencyKey)
|
package/dist/upload.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ToolDescriptor } from './manifest.js';
|
|
1
2
|
import { type ToolContext } from './toolSurface.js';
|
|
2
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;
|
|
@@ -6,11 +7,20 @@ export interface UploadLimits {
|
|
|
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;
|