@ciphrix/cli 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +201 -0
  3. package/dist/actions.d.ts +19 -0
  4. package/dist/actions.js +30 -0
  5. package/dist/api.d.ts +27 -0
  6. package/dist/api.js +180 -0
  7. package/dist/banner.d.ts +2 -0
  8. package/dist/banner.js +12 -0
  9. package/dist/browser.d.ts +2 -0
  10. package/dist/browser.js +44 -0
  11. package/dist/cli.d.ts +14 -0
  12. package/dist/cli.js +1579 -0
  13. package/dist/commands/asset.d.ts +59 -0
  14. package/dist/commands/asset.js +237 -0
  15. package/dist/commands/check.d.ts +30 -0
  16. package/dist/commands/check.js +182 -0
  17. package/dist/commands/context.d.ts +23 -0
  18. package/dist/commands/context.js +102 -0
  19. package/dist/commands/control.d.ts +44 -0
  20. package/dist/commands/control.js +189 -0
  21. package/dist/commands/framework.d.ts +40 -0
  22. package/dist/commands/framework.js +181 -0
  23. package/dist/commands/job.d.ts +16 -0
  24. package/dist/commands/job.js +30 -0
  25. package/dist/commands/login.d.ts +12 -0
  26. package/dist/commands/login.js +54 -0
  27. package/dist/commands/logout.d.ts +9 -0
  28. package/dist/commands/logout.js +56 -0
  29. package/dist/commands/policy.d.ts +23 -0
  30. package/dist/commands/policy.js +112 -0
  31. package/dist/commands/policyActions.d.ts +37 -0
  32. package/dist/commands/policyActions.js +177 -0
  33. package/dist/commands/risk.d.ts +51 -0
  34. package/dist/commands/risk.js +289 -0
  35. package/dist/commands/test.d.ts +54 -0
  36. package/dist/commands/test.js +331 -0
  37. package/dist/commands/vendor.d.ts +45 -0
  38. package/dist/commands/vendor.js +218 -0
  39. package/dist/config.d.ts +12 -0
  40. package/dist/config.js +56 -0
  41. package/dist/constants.d.ts +7 -0
  42. package/dist/constants.js +7 -0
  43. package/dist/credentials.d.ts +19 -0
  44. package/dist/credentials.js +182 -0
  45. package/dist/deviceFlow.d.ts +32 -0
  46. package/dist/deviceFlow.js +115 -0
  47. package/dist/deviceInfo.d.ts +11 -0
  48. package/dist/deviceInfo.js +25 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +4 -0
  51. package/dist/io.d.ts +46 -0
  52. package/dist/io.js +99 -0
  53. package/dist/output.d.ts +14 -0
  54. package/dist/output.js +37 -0
  55. package/dist/theme.d.ts +25 -0
  56. package/dist/theme.js +39 -0
  57. package/dist/toolSurface.d.ts +20 -0
  58. package/dist/toolSurface.js +49 -0
  59. package/dist/upload.d.ts +18 -0
  60. package/dist/upload.js +115 -0
  61. package/package.json +71 -0
  62. package/skills/ciphrix/SKILL.md +169 -0
  63. package/skills/ciphrix/references/commands.md +1592 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Colour handling for terminal output.
3
+ *
4
+ * Rules:
5
+ * - `NO_COLOR` (non-empty) disables colour, per https://no-color.org.
6
+ * - `FORCE_COLOR` (non-empty, not `0`) enables colour even when piped.
7
+ * - Otherwise colour is enabled only for an interactive TTY.
8
+ *
9
+ * Decoration must never leak into machine-readable output; callers gate that
10
+ * with the resolved `color` flag.
11
+ */
12
+ export interface Theme {
13
+ readonly color: boolean;
14
+ bold(text: string): string;
15
+ dim(text: string): string;
16
+ cyan(text: string): string;
17
+ green(text: string): string;
18
+ red(text: string): string;
19
+ yellow(text: string): string;
20
+ grey(text: string): string;
21
+ }
22
+ export declare const colorEnabled: (stream?: Pick<NodeJS.WriteStream, "isTTY">, env?: NodeJS.ProcessEnv) => boolean;
23
+ export declare const createTheme: (color?: boolean) => Theme;
24
+ /** Restore only unguessable markers created by this module; forged marker-like text is discarded. */
25
+ export declare const renderThemeStyles: (text: string) => string;
package/dist/theme.js ADDED
@@ -0,0 +1,39 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const OPEN = '\uE000CXI';
3
+ const CLOSE = '\uE001';
4
+ const issuedMarkerIds = new Set();
5
+ const STYLE_CODES = {
6
+ '0': '\u001B[0m',
7
+ '1': '\u001B[1m',
8
+ '2': '\u001B[2m',
9
+ '31': '\u001B[31m',
10
+ '32': '\u001B[32m',
11
+ '33': '\u001B[33m',
12
+ '36': '\u001B[36m',
13
+ '90': '\u001B[90m',
14
+ };
15
+ const isSet = (value) => typeof value === 'string' && value !== '';
16
+ export const colorEnabled = (stream = process.stdout, env = process.env) => {
17
+ if (isSet(env.FORCE_COLOR) && env.FORCE_COLOR !== '0')
18
+ return true;
19
+ if (isSet(env.NO_COLOR))
20
+ return false;
21
+ return stream.isTTY === true;
22
+ };
23
+ const paint = (open, text, color, markerId) => color ? `${OPEN}${markerId}:${open}${CLOSE}${text}${OPEN}${markerId}:0${CLOSE}` : text;
24
+ export const createTheme = (color = colorEnabled()) => {
25
+ const markerId = randomUUID();
26
+ issuedMarkerIds.add(markerId);
27
+ return {
28
+ color,
29
+ bold: (text) => paint(1, text, color, markerId),
30
+ dim: (text) => paint(2, text, color, markerId),
31
+ cyan: (text) => paint(36, text, color, markerId),
32
+ green: (text) => paint(32, text, color, markerId),
33
+ red: (text) => paint(31, text, color, markerId),
34
+ yellow: (text) => paint(33, text, color, markerId),
35
+ grey: (text) => paint(90, text, color, markerId),
36
+ };
37
+ };
38
+ /** Restore only unguessable markers created by this module; forged marker-like text is discarded. */
39
+ export const renderThemeStyles = (text) => text.replace(/\uE000CXI([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):(0|1|2|31|32|33|36|90)\uE001/gi, (marker, markerId, code) => issuedMarkerIds.has(markerId.toLowerCase()) ? (STYLE_CODES[code] ?? '') : '');
@@ -0,0 +1,20 @@
1
+ import { type ApiClient } from './api.js';
2
+ import { type CredentialStore } from './credentials.js';
3
+ export declare const asRecord: (value: unknown) => Record<string, unknown>;
4
+ export declare const asText: (value: unknown, fallback?: string) => string;
5
+ /**
6
+ * The message a caller should see. Ambiguous-name errors carry the candidates, so they are rendered here —
7
+ * otherwise the user is told "use a code" without being shown which codes exist.
8
+ */
9
+ export declare const errorMessage: (envelope: Record<string, unknown>, fallback: string) => string;
10
+ export interface ToolContext {
11
+ baseUrl: string;
12
+ client: ApiClient;
13
+ token: string;
14
+ }
15
+ export declare const resolveToolContext: (apiUrl?: string, store?: CredentialStore) => Promise<ToolContext>;
16
+ export interface CallToolOptions {
17
+ confirmationToken?: string | undefined;
18
+ idempotencyKey?: string | undefined;
19
+ }
20
+ export declare const callTool: (ctx: ToolContext, toolName: string, input: unknown, options?: CallToolOptions) => Promise<Record<string, unknown>>;
@@ -0,0 +1,49 @@
1
+ import { createApiClient } from './api.js';
2
+ import { resolveApiBaseUrl } from './config.js';
3
+ import { createCredentialStore } from './credentials.js';
4
+ export const asRecord = (value) => value && typeof value === 'object' ? value : {};
5
+ export const asText = (value, fallback = '') => typeof value === 'string' ? value : fallback;
6
+ /**
7
+ * The message a caller should see. Ambiguous-name errors carry the candidates, so they are rendered here —
8
+ * otherwise the user is told "use a code" without being shown which codes exist.
9
+ */
10
+ export const errorMessage = (envelope, fallback) => {
11
+ const error = asRecord(envelope.error);
12
+ const base = asText(error.message, fallback);
13
+ if (error.code === 'ambiguous' && Array.isArray(error.details)) {
14
+ const candidates = error.details
15
+ .map((item) => {
16
+ const record = asRecord(item);
17
+ const code = asText(record.code);
18
+ const name = asText(record.name);
19
+ if (code && name)
20
+ return `${code} ${name}`;
21
+ return code || name || asText(record.id);
22
+ })
23
+ .filter((value) => value !== '');
24
+ if (candidates.length > 0)
25
+ return `${base}\n ${candidates.join('\n ')}`;
26
+ }
27
+ return base;
28
+ };
29
+ export const resolveToolContext = async (apiUrl, store) => {
30
+ const baseUrl = resolveApiBaseUrl({ flag: apiUrl });
31
+ const credentialStore = store ?? (await createCredentialStore());
32
+ const credential = await credentialStore.get(baseUrl);
33
+ if (!credential)
34
+ throw new Error('Not signed in. Run `ciphrix login` first.');
35
+ return { baseUrl, client: createApiClient(baseUrl), token: credential.token };
36
+ };
37
+ export const callTool = async (ctx, toolName, input, options = {}) => {
38
+ const body = { input };
39
+ if (options.confirmationToken)
40
+ body.confirmationToken = options.confirmationToken;
41
+ if (options.idempotencyKey)
42
+ body.idempotencyKey = options.idempotencyKey;
43
+ const data = await ctx.client.request(`/tools/v1/${encodeURIComponent(toolName)}`, {
44
+ method: 'POST',
45
+ token: ctx.token,
46
+ body,
47
+ });
48
+ return asRecord(data);
49
+ };
@@ -0,0 +1,18 @@
1
+ import { type ToolContext } from './toolSurface.js';
2
+ /** Maximum evidence file size read and staged by the CLI (50 MiB). */
3
+ export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
4
+ export interface UploadLimits {
5
+ maxUploadBytes?: number;
6
+ timeoutMs?: number;
7
+ maxResponseBytes?: number;
8
+ }
9
+ /**
10
+ * Stages a local file through the `upload_file` tool and returns its upload id, ready to be attached
11
+ * to a target (a test run, a risk, a vendor). The file is sent as multipart to the tool surface with
12
+ * the bearer credential, so no cookie or CSRF token is involved.
13
+ */
14
+ export declare const uploadLocalFile: (ctx: ToolContext, filePath: string, purpose: string, limits?: UploadLimits) => Promise<{
15
+ id: string;
16
+ filename: string;
17
+ size: number;
18
+ }>;
package/dist/upload.js ADDED
@@ -0,0 +1,115 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open } from 'node:fs/promises';
4
+ import { basename } from 'node:path';
5
+ import { ApiError, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_REQUEST_TIMEOUT_MS, fetchWithoutRedirects, readLimitedText, withRequestTimeout, } from './api.js';
6
+ import { asRecord } from './toolSurface.js';
7
+ const MIME_TYPES = {
8
+ '.pdf': 'application/pdf',
9
+ '.png': 'image/png',
10
+ '.jpg': 'image/jpeg',
11
+ '.jpeg': 'image/jpeg',
12
+ '.gif': 'image/gif',
13
+ '.txt': 'text/plain',
14
+ '.md': 'text/markdown',
15
+ '.csv': 'text/csv',
16
+ '.json': 'application/json',
17
+ '.doc': 'application/msword',
18
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
19
+ '.xls': 'application/vnd.ms-excel',
20
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
21
+ };
22
+ /** Maximum evidence file size read and staged by the CLI (50 MiB). */
23
+ export const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
24
+ const readUploadFile = async (filePath, maxBytes) => {
25
+ const initialStat = await lstat(filePath);
26
+ if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
27
+ throw new Error('Upload path must be a regular file, not a symlink or special file.');
28
+ }
29
+ if (initialStat.size > maxBytes) {
30
+ throw new Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
31
+ }
32
+ const noFollow = constants.O_NOFOLLOW ?? 0;
33
+ const file = await open(filePath, constants.O_RDONLY | noFollow);
34
+ try {
35
+ const openedStat = await file.stat();
36
+ if (!openedStat.isFile() ||
37
+ openedStat.dev !== initialStat.dev ||
38
+ openedStat.ino !== initialStat.ino) {
39
+ throw new Error('Upload path changed while it was being opened. Please retry.');
40
+ }
41
+ if (openedStat.size > maxBytes) {
42
+ throw new Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
43
+ }
44
+ const chunks = [];
45
+ let total = 0;
46
+ while (true) {
47
+ const remaining = maxBytes + 1 - total;
48
+ if (remaining <= 0)
49
+ throw new Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
50
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
51
+ const { bytesRead } = await file.read(chunk, 0, chunk.length, null);
52
+ if (bytesRead === 0)
53
+ break;
54
+ total += bytesRead;
55
+ if (total > maxBytes)
56
+ throw new Error(`Upload exceeds the ${maxBytes}-byte file size limit.`);
57
+ chunks.push(chunk.subarray(0, bytesRead));
58
+ }
59
+ return Buffer.concat(chunks, total);
60
+ }
61
+ finally {
62
+ await file.close();
63
+ }
64
+ };
65
+ const contentTypeFor = (filename) => {
66
+ const dot = filename.lastIndexOf('.');
67
+ const extension = dot === -1 ? '' : filename.slice(dot).toLowerCase();
68
+ return MIME_TYPES[extension] ?? 'application/octet-stream';
69
+ };
70
+ /**
71
+ * Stages a local file through the `upload_file` tool and returns its upload id, ready to be attached
72
+ * to a target (a test run, a risk, a vendor). The file is sent as multipart to the tool surface with
73
+ * the bearer credential, so no cookie or CSRF token is involved.
74
+ */
75
+ export const uploadLocalFile = async (ctx, filePath, purpose, limits = {}) => {
76
+ const filename = basename(filePath);
77
+ const contents = await readUploadFile(filePath, limits.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES);
78
+ const form = new FormData();
79
+ form.append('purpose', purpose);
80
+ form.append('file', new Blob([new Uint8Array(contents)], { type: contentTypeFor(filename) }), filename);
81
+ return withRequestTimeout(limits.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, async (signal) => {
82
+ const response = await fetchWithoutRedirects(`${ctx.baseUrl}/tools/v1/upload_file`, {
83
+ method: 'POST',
84
+ headers: { authorization: `Bearer ${ctx.token}`, 'idempotency-key': randomUUID() },
85
+ body: form,
86
+ signal,
87
+ });
88
+ const text = await readLimitedText(response, limits.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
89
+ let data = null;
90
+ if (text) {
91
+ try {
92
+ data = JSON.parse(text);
93
+ }
94
+ catch {
95
+ data = text;
96
+ }
97
+ }
98
+ const envelope = asRecord(data);
99
+ if (!response.ok || envelope.status !== 'ok') {
100
+ const message = typeof asRecord(envelope.error).message === 'string'
101
+ ? asRecord(envelope.error).message
102
+ : `Upload failed with status ${response.status}`;
103
+ throw new ApiError(message, response.status);
104
+ }
105
+ const payload = asRecord(envelope.data);
106
+ const id = typeof payload.uploadId === 'string' ? payload.uploadId : null;
107
+ if (!id)
108
+ throw new ApiError('Upload did not return an id', 502);
109
+ return {
110
+ id,
111
+ filename: typeof payload.filename === 'string' ? payload.filename : filename,
112
+ size: typeof payload.size === 'number' ? payload.size : contents.length,
113
+ };
114
+ });
115
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@ciphrix/cli",
3
+ "version": "0.1.0",
4
+ "description": "Official command-line interface for the Ciphrix compliance platform.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "ciphrix": "dist/index.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "exports": {
12
+ ".": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "skills",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20.19.0"
22
+ },
23
+ "packageManager": "npm@11.19.1",
24
+ "scripts": {
25
+ "clean": "node scripts/clean.mjs",
26
+ "build": "npm run clean && tsc -p tsconfig.build.json",
27
+ "postbuild": "node scripts/set-bin-mode.mjs",
28
+ "ci": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build && npm run commands:check && npm run package:check",
29
+ "dev": "tsx src/index.ts",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "lint": "eslint .",
32
+ "lint:fix": "eslint . --fix",
33
+ "format": "prettier --write .",
34
+ "format:check": "prettier --check .",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "commands:generate": "npm run build && node scripts/generate-command-reference.mjs",
38
+ "commands:check": "node scripts/generate-command-reference.mjs --check",
39
+ "package:check": "node scripts/check-package-contents.mjs",
40
+ "prepack": "npm run build",
41
+ "audit:render": "node scripts/render-audit.mjs"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/CiphrixHQ/ciphrix-cli.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/CiphrixHQ/ciphrix-cli/issues"
49
+ },
50
+ "homepage": "https://github.com/CiphrixHQ/ciphrix-cli#readme",
51
+ "keywords": [
52
+ "ciphrix",
53
+ "compliance",
54
+ "cli",
55
+ "security"
56
+ ],
57
+ "dependencies": {
58
+ "@napi-rs/keyring": "^2.1.0",
59
+ "commander": "^15.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
63
+ "@types/node": "^26.6.2",
64
+ "eslint": "^10.11.0",
65
+ "prettier": "^3.9.8",
66
+ "tsx": "^4.23.15",
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.70.1",
69
+ "vitest": "^5.0.1"
70
+ }
71
+ }
@@ -0,0 +1,169 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Ciphrix
7
+
8
+ Ciphrix is an AI-native compliance automation platform. A tenant's compliance programme lives in one
9
+ place: how the organization operates, the documents it publishes, the frameworks it has adopted, the
10
+ tests those frameworks impose, the evidence gathered against those tests, and the risks, vendors, assets
11
+ and monitoring checks around them. AI does the heavy lifting — generating drafts, reading evidence,
12
+ judging relevance — but every formal decision stays with a human.
13
+
14
+ You are operating that programme through `ciphrix`. You resolve things by **name or code**, you act on
15
+ **intents**, and you report what actually happened — including when nothing did.
16
+
17
+ ## Operating stance
18
+
19
+ Ciphrix treats compliance as a living model of how an organization operates, not a checklist assembled
20
+ for an audit. Context explains the organization; documents state how it intends to operate; frameworks,
21
+ clauses and controls express obligations and design; tests ask whether those claims hold; evidence and
22
+ monitoring show what actually happened; risks, vendors and assets connect that assurance to the real
23
+ business. Preserve those relationships when you work.
24
+
25
+ Apply these principles:
26
+
27
+ - **Reality before paperwork.** Start from the organization's actual context and current records. Do not
28
+ manufacture a polished compliance story that is unsupported by the system.
29
+ - **Evidence before assertion.** Treat evidence, monitoring results and recorded decisions as distinct
30
+ facts. A document saying that a control exists is not proof that it operated, and an AI assessment is
31
+ not a human decision.
32
+ - **Assistance without invented authority.** Help people understand, draft, organise and execute their
33
+ intent. Do not approve, attest, accept risk, record a test result or make another formal decision unless
34
+ the user clearly asked for that decision.
35
+ - **The API is the source of truth.** Never infer access, state or success from expectations. Report the
36
+ result returned by Ciphrix, preserve distinctions such as `not found`, `not permitted`, `pending` and
37
+ `not assessed`, and never work around authorization.
38
+ - **Small, attributable changes.** Read the relevant record before changing it, update only the requested
39
+ fields, and report the resulting state. Do not turn a narrow request into broad programme cleanup.
40
+ - **Human-readable by default, structured when needed.** Use normal output when working with a person.
41
+ Use `--json` when exact fields, pagination or reliable machine processing matter; do not scrape
42
+ decorated terminal output.
43
+
44
+ ## How the platform is organised
45
+
46
+ Understanding these relationships is most of the job:
47
+
48
+ - **Context** is the foundation. _Business Context_ describes how the organization actually operates;
49
+ _Operating Context_ scopes the document and compliance programme. Everything else — discovery, document
50
+ generation, risk and vendor suggestions, tests — is grounded in this. Answers merge over time; a new
51
+ answer that conflicts with an existing one goes to review instead of silently overwriting.
52
+ - **Frameworks** (SOC 2, ISO 27001, …) are applied to the tenant and bring their **clauses** and
53
+ **tests** with them.
54
+ - A **test** is a single requirement check. Each test has **runs**, one per month — the server enforces
55
+ that cadence. A run collects **evidence items**: uploaded files, native documents, or results from
56
+ monitoring **checks**. Attaching evidence starts an AI pipeline that digests the item, judges its
57
+ **relevance** to the test, and produces an **assurance** view. A human then records the **run
58
+ result** (`passing`, `failing`, `skipped`, or back to `pending`), which is a formal decision.
59
+ - **Documents** (the Document Library) have a lifecycle: `draft` → review → approval → `approved`. Only a
60
+ draft is editable; changing an approved document means creating a new version.
61
+ - **Files** are uploads and the file system. Keep the two words apart: the Document Library holds design
62
+ documents; Files holds uploaded files.
63
+ - **Risks**, **vendors** and **assets** are registers with their own state (status, owner, criticality,
64
+ review status, classification). Each has a stable code, and state changes are auditable. A risk also
65
+ carries a **treatment** (strategy and notes) and **scores**; an asset carries **tags** and is mapped to
66
+ controls, risks, tests and checks.
67
+ - **Clauses** and **controls** are what you are assessed against. A clause belongs to a framework and
68
+ carries an applicability and a design-requirement assessment; a control is the tenant's own layer with
69
+ a status, an owner, an applicability and design requirements.
70
+ - **Monitoring checks** run continuously against connected integrations and produce results on their own.
71
+ A check can be turned on or off for the tenant, and its run history and linked findings are readable.
72
+
73
+ Two consequences worth internalising:
74
+
75
+ - **AI output is never a verdict.** An assessment that hasn't been evaluated reads as _not assessed_ —
76
+ never a guess. Honest absence beats a plausible answer.
77
+ - **Not found ≠ no access ≠ not assessed.** These are different facts and are reported differently.
78
+ Do not collapse them.
79
+
80
+ ## How to operate
81
+
82
+ - **Names or codes, not ids.** Documents, tests, controls, clauses, risks, vendors, checks and assets are
83
+ addressed by their human name **or** their stable code (for example `ACME-DOC-A1B2`, `TSQ-001`,
84
+ `ACME-RSK-CQJ4`, `ACME-AST-M5SD`). Ids work but are never required, and you never invent one. Prefer the
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.
88
+ - **Ask only for decisions.** Only actions that change something a person should review stop for
89
+ confirmation — deleting, editing content, linking, or changing a control, clause, test or asset. A
90
+ formal decision is its own verb (`approve`, `submit`, `test run result`) and applies directly. Everything
91
+ else just happens. Use `--yes` only when the user has already authorised that exact change; it removes
92
+ an interactive prompt, not the need for authority.
93
+ - **Async work returns a job id.** Attaching evidence or starting AI work returns a job id immediately and
94
+ does not block; report the id and let the user track it with `ciphrix job status`.
95
+ - **Rich text is Markdown.** Document content and descriptions come back as Markdown, and you supply
96
+ Markdown.
97
+ - **Lists paginate.** You'll see `showing 1–25 of N · next: --page 2`; page through rather than assuming
98
+ you have everything.
99
+ - **`--json`** gives the raw payload when you need exact fields.
100
+
101
+ ## Patterns
102
+
103
+ These are the shapes of the work; run `ciphrix <resource> --help` for the exact flags.
104
+
105
+ **Answer "what does the platform know about us?"** Start with context: read Business and Operating
106
+ Context, and check what's still unanswered. This is the usual first move when a request is broad, because
107
+ everything downstream depends on it.
108
+
109
+ **Change a document.** Read it, decide whether the change is **content** or **metadata**, and act
110
+ accordingly. If the document is approved, you must open a new version first — editing an approved document
111
+ is refused by design. Publishing and approving are formal, separate steps; don't fold them into the edit.
112
+ When drafting content, preserve the user's intended policy position and clearly separate current practice
113
+ from proposals or future commitments.
114
+
115
+ **Attach evidence to a test.** Find the test, then attach in one step with a local file
116
+ (`test attach "<test>" --file <path>`). The command stages the file and attaches it; you do not need to
117
+ stage and attach separately. Only stage a file on its own when you intend to attach it later or reuse it
118
+ across several items. After attaching, report the returned job id and point at `ciphrix job status` — the
119
+ relevance judgement is not instant.
120
+
121
+ **See what you're assessed against.** A framework holds **clauses**; each clause carries an applicability
122
+ and a design-requirement assessment and is mapped to tests and documents. Read the clauses, then the one
123
+ that matters. **Controls** are the tenant's own layer over that: `control list` and `control get` show the
124
+ status, owner, applicability and design requirements, and `control items` shows what is mapped to it.
125
+ Ownership, applicability, design requirements and notes are yours to change; a control's or test's name and
126
+ description belong to the catalogue and are refused for system records (use `--rename` only on custom ones).
127
+
128
+ **Record a result.** A run result is a decision, not a note. `pending` is not "failed" — it reopens the
129
+ run for more evidence. Use it deliberately.
130
+
131
+ **Triage a risk, vendor or asset.** Read the current record first, then update only the fields you're
132
+ changing. Report which fields applied and which were refused; don't assume success. A risk's treatment and
133
+ scores are their own reads; a vendor's or risk's files are listed and changed separately from the record
134
+ itself.
135
+
136
+ **Work the checks.** `check list` shows status and compliance; `check runs` is the history; `check findings`
137
+ are what a check raised. `check disable` silences a check for the tenant — record why with `--notes`.
138
+
139
+ **Check on background work.** Anything that returns a job id is polled, not awaited.
140
+
141
+ ## Guardrails
142
+
143
+ - Work only in the tenant and API environment the authenticated user selected. Never discover or switch
144
+ tenants, endpoints or credential stores by guessing.
145
+ - Do not expose access tokens, stored credentials or sensitive response data in prose, logs or commands.
146
+ Prefer the OS keychain. File-based credential storage and plaintext HTTP are explicit local/testing
147
+ opt-ins, never defaults.
148
+ - Do not retry denied actions through another command, weaken authorization, or reinterpret an access
149
+ failure as a missing record.
150
+ - Don't edit an approved document; create a new version.
151
+ - Don't treat `pending` as a failure or as "done".
152
+ - Don't invent AI assessments. If it isn't evaluated, say so.
153
+ - Don't guess when a name is ambiguous — the CLI lists the candidates and stops; take that as your answer
154
+ and ask the user.
155
+ - Don't stage a file and then attach it when a single attach-with-file command exists.
156
+ - Before a consequential write, make sure the target resolved to the intended record. After the write,
157
+ report the server's outcome rather than claiming success from the command exit alone.
158
+
159
+ ## Finding exact syntax
160
+
161
+ The command surface is discoverable and always current — don't memorise or infer it:
162
+
163
+ - `ciphrix --help` — the resources and verbs.
164
+ - `ciphrix <resource> --help` — flags for a resource.
165
+ - `ciphrix <resource> <verb> --help` — exact arguments and write semantics.
166
+ - [`references/commands.md`](references/commands.md) — generated command and option reference for offline discovery; live help from the installed CLI remains authoritative.
167
+
168
+ If the CLI does not expose the requested capability, say so. Do not call private endpoints, reproduce
169
+ backend logic, or invent a command from this guide.