@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/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';
@@ -21,13 +22,15 @@ const MIME_TYPES = {
21
22
  };
22
23
  /** Maximum evidence file size read and staged by the CLI (25 MiB, matching the tool surface). */
23
24
  export const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
24
- const readUploadFile = async (filePath, maxBytes) => {
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 Error('Upload path must be a regular file, not a symlink or special file.');
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 Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
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 Error('Upload path changed while it was being opened. Please retry.');
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 Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
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 Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
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 Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
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 uploadLocalFile = async (ctx, filePath, purpose, limits = {}) => {
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/upload_file`, {
95
+ const response = await fetchWithoutRedirects(`${ctx.baseUrl}/tools/v1/${encodeURIComponent(tool.name)}`, {
83
96
  method: 'POST',
84
- headers: { authorization: `Bearer ${ctx.token}`, 'idempotency-key': randomUUID() },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciphrix/cli",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official command-line interface for the Ciphrix compliance platform.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ciphrix
3
- description: Operate a Ciphrix compliance programme with the `ciphrix` CLI. Use when an agent needs to understand or change a tenant's business context, documents, frameworks, controls, tests and evidence, risks, vendors, assets, monitoring checks, or background jobs.
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
@@ -83,8 +83,9 @@ Two consequences worth internalising:
83
83
  addressed by their human name **or** their stable code (for example `ACME-DOC-A1B2`, `TSQ-001`,
84
84
  `ACME-RSK-CQJ4`, `ACME-AST-M5SD`). Ids work but are never required, and you never invent one. Prefer the
85
85
  code when a name is ambiguous.
86
- - **Intents, not steps.** Ask for the outcome; don't orchestrate the underlying calls. One user-facing
87
- request may be several platform operations — that composition is the CLI's job, not the user's.
86
+ - **Intents, not hidden decisions.** An agent may coordinate explicitly authorized platform operations
87
+ for one user request. Each operation remains separate: stop on errors or uncertain outcomes and never
88
+ infer a formal decision merely because it would let the next step proceed.
88
89
  - **Ask only for decisions.** Only actions that change something a person should review stop for
89
90
  confirmation — deleting, editing content, linking, or changing a control, clause, test or asset. A
90
91
  formal decision is its own verb (`approve`, `submit`, `test run result`) and applies directly. Everything
@@ -140,6 +141,44 @@ are what a check raised. `check disable` silences a check for the tenant — rec
140
141
 
141
142
  ## Guardrails
142
143
 
144
+ **Document workflow.** Configuration, request sending, review completion, ordinary approval and
145
+ self-approval are separate intents. Sending for approval never authorizes completing review first.
146
+ When a recipient setter reports an already-sent request, explain the current stage and offer recall,
147
+ configure and resend, even if the requested recipient is unchanged. Proceed only with authorization;
148
+ recall clears the recipient, so set them again before resending rather than removing an already-cleared
149
+ assignment. Unsent same-recipient selection reports already set. Read again between recovery steps and
150
+ use a separate write key per operation. Employees may be assigned and complete decisions in the app;
151
+ user discovery does not grant tool access. Use exact active User IDs/emails, not People IDs or fuzzy
152
+ name matches. Workflow readiness is business guidance, not permission or proof of notification delivery.
153
+ Before coordinating a document request or recovery, read
154
+ [the workflow guide](references/document-workflows.md) for stage-specific sequencing and result handling;
155
+ use installed help or [the generated reference](references/commands.md) for exact syntax.
156
+
157
+ **Context and onboarding.** Read the server status and question schema, then save only facts agreed
158
+ with the user. Business and Operating Context are separate. Let the server decide readiness; initial
159
+ setup is optional and journey completion is explicit. A context save never authorizes starting agents,
160
+ initializing a framework or completing onboarding. Replayed packs are snapshots: read status again for
161
+ current readiness.
162
+
163
+ **Access reviews and proposals.** Inventory is not a review decision. Inspect proposal rows, matches,
164
+ source evidence and current revisions before selecting a merge. Never manufacture person-match or
165
+ visual-evidence confirmation. Keep/revoke decisions, reviewer-task completion and campaign ending are
166
+ distinct human intents. A revoke decision is a removal request, not provider-side revocation. Agent
167
+ findings/options and AI assessments are advisory; inspect the complete current proposal and exact
168
+ declared option before any explicit decision. Assigned-task discovery grants no completion authority.
169
+
170
+ **Writes and uncertainty.** Discover capabilities through the server, use the exact reviewed inputs,
171
+ and treat confirmation as explicit delegated consent, not an obstacle to automate around. Retain one
172
+ idempotency key per logical operation; never retry unknown outcomes with a new key. The CLI performs no
173
+ automatic write retries and does not promise confirmed recovery across invocations or expired tokens.
174
+ Inspect state/jobs when uncertain. Batch results can partially apply; preserve and report failures.
175
+ Advisory assessments never authorize a formal passing result.
176
+
177
+ **Files.** Stage and process only the files the user selected. General processing may update context
178
+ or create vendor records; staging alone does not authorize that processing. Imports are proposed work,
179
+ not merged inventory. Temporary evidence/export references expire; do not forward credentials to them,
180
+ claim bytes were downloaded when only a reference was returned, or treat a replayed URL as renewed.
181
+
143
182
  - Work only in the tenant and API environment the authenticated user selected. Never discover or switch
144
183
  tenants, endpoints or credential stores by guessing.
145
184
  - Do not expose access tokens, stored credentials or sensitive response data in prose, logs or commands.