compound-workflow 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +12 -0
- package/.cursor-plugin/plugin.json +12 -0
- package/README.md +155 -0
- package/package.json +22 -0
- package/scripts/install-cli.mjs +313 -0
- package/scripts/sync-into-repo.sh +103 -0
- package/src/.agents/agents/research/best-practices-researcher.md +132 -0
- package/src/.agents/agents/research/framework-docs-researcher.md +134 -0
- package/src/.agents/agents/research/git-history-analyzer.md +62 -0
- package/src/.agents/agents/research/learnings-researcher.md +288 -0
- package/src/.agents/agents/research/repo-research-analyst.md +146 -0
- package/src/.agents/agents/review/agent-native-reviewer.md +299 -0
- package/src/.agents/agents/workflow/bug-reproduction-validator.md +87 -0
- package/src/.agents/agents/workflow/lint.md +20 -0
- package/src/.agents/agents/workflow/spec-flow-analyzer.md +149 -0
- package/src/.agents/commands/assess.md +60 -0
- package/src/.agents/commands/install.md +53 -0
- package/src/.agents/commands/metrics.md +59 -0
- package/src/.agents/commands/setup.md +9 -0
- package/src/.agents/commands/sync.md +9 -0
- package/src/.agents/commands/test-browser.md +393 -0
- package/src/.agents/commands/workflow/brainstorm.md +252 -0
- package/src/.agents/commands/workflow/compound.md +142 -0
- package/src/.agents/commands/workflow/plan.md +737 -0
- package/src/.agents/commands/workflow/review-v2.md +148 -0
- package/src/.agents/commands/workflow/review.md +110 -0
- package/src/.agents/commands/workflow/triage.md +54 -0
- package/src/.agents/commands/workflow/work.md +439 -0
- package/src/.agents/references/README.md +12 -0
- package/src/.agents/references/standards/README.md +9 -0
- package/src/.agents/scripts/self-check.mjs +227 -0
- package/src/.agents/scripts/sync-opencode.mjs +355 -0
- package/src/.agents/skills/agent-browser/SKILL.md +223 -0
- package/src/.agents/skills/audit-traceability/SKILL.md +260 -0
- package/src/.agents/skills/brainstorming/SKILL.md +250 -0
- package/src/.agents/skills/compound-docs/SKILL.md +533 -0
- package/src/.agents/skills/compound-docs/assets/critical-pattern-template.md +34 -0
- package/src/.agents/skills/compound-docs/assets/resolution-template.md +97 -0
- package/src/.agents/skills/compound-docs/references/yaml-schema.md +87 -0
- package/src/.agents/skills/compound-docs/schema.project.yaml +18 -0
- package/src/.agents/skills/compound-docs/schema.yaml +119 -0
- package/src/.agents/skills/data-foundations/SKILL.md +185 -0
- package/src/.agents/skills/document-review/SKILL.md +108 -0
- package/src/.agents/skills/file-todos/SKILL.md +177 -0
- package/src/.agents/skills/file-todos/assets/todo-template.md +106 -0
- package/src/.agents/skills/financial-workflow-integrity/SKILL.md +423 -0
- package/src/.agents/skills/git-worktree/SKILL.md +268 -0
- package/src/.agents/skills/pii-protection-prisma/SKILL.md +629 -0
- package/src/.agents/skills/process-metrics/SKILL.md +46 -0
- package/src/.agents/skills/process-metrics/assets/daily-template.md +37 -0
- package/src/.agents/skills/process-metrics/assets/monthly-template.md +21 -0
- package/src/.agents/skills/process-metrics/assets/weekly-template.md +25 -0
- package/src/.agents/skills/technical-review/SKILL.md +83 -0
- package/src/AGENTS.md +213 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pii-protection-prisma
|
|
3
|
+
description: Enforce PII table separation + envelope encryption + KEK rotation for Prisma + Postgres.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PII Protection & Encryption Standard (Prisma + Postgres)
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Provide an enforceable, production-grade standard for storing and handling PII in systems that may be regulated or financially sensitive.
|
|
11
|
+
|
|
12
|
+
This skill is designed to be followed as a build-time guardrail:
|
|
13
|
+
|
|
14
|
+
- clear rules (MUST / SHOULD / MUST NOT)
|
|
15
|
+
- concrete schema requirements
|
|
16
|
+
- concrete encryption + rotation approach
|
|
17
|
+
- operational failure modes + runbooks
|
|
18
|
+
- Prisma + Postgres reference implementation
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Scope
|
|
23
|
+
|
|
24
|
+
Applies to any system that stores or processes:
|
|
25
|
+
|
|
26
|
+
- identity information (name, DOB, address, document ids)
|
|
27
|
+
- contact details (email/phone) when linked to an individual
|
|
28
|
+
- government identifiers
|
|
29
|
+
- bank identifiers (BSB/account), tax identifiers
|
|
30
|
+
- uploaded documents containing personal information
|
|
31
|
+
- free-text fields where users may provide personal info
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Definitions
|
|
36
|
+
|
|
37
|
+
- PII: personally identifiable information.
|
|
38
|
+
- DEK: Data Encryption Key. Random per-record symmetric key used to encrypt PII payload.
|
|
39
|
+
- KEK: Key Encryption Key. A master key stored in a KMS used to wrap (encrypt) DEKs.
|
|
40
|
+
- Envelope Encryption: Encrypt data with a DEK; encrypt the DEK with a KEK; store encrypted DEK + ciphertext.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Non-Negotiable Rules (MUST / MUST NOT)
|
|
45
|
+
|
|
46
|
+
### Data placement
|
|
47
|
+
|
|
48
|
+
- MUST store PII outside primary business tables.
|
|
49
|
+
- MUST NOT store PII inside `applications.draftData` or any general JSON blob in a business table.
|
|
50
|
+
- MUST store PII in a dedicated table (1:1 with owning entity) using encrypted columns.
|
|
51
|
+
- MUST keep audit logs free of plaintext PII.
|
|
52
|
+
|
|
53
|
+
### Encryption
|
|
54
|
+
|
|
55
|
+
- MUST use envelope encryption by default for PII at rest.
|
|
56
|
+
- MUST use an AEAD cipher (AES-256-GCM) for PII payload encryption.
|
|
57
|
+
- MUST generate a unique random IV/nonce per encryption.
|
|
58
|
+
- MUST bind ciphertext to record identity using AAD (Associated Authenticated Data).
|
|
59
|
+
- MUST store `keyId` and `schemaVersion` per record.
|
|
60
|
+
- MUST NOT use deterministic encryption for general PII fields.
|
|
61
|
+
|
|
62
|
+
### Key management
|
|
63
|
+
|
|
64
|
+
- MUST store KEKs in a KMS or secure secret manager (not the DB).
|
|
65
|
+
- MUST support multiple active key versions for decryption.
|
|
66
|
+
- MUST support key rotation without downtime.
|
|
67
|
+
|
|
68
|
+
### Logging & analytics
|
|
69
|
+
|
|
70
|
+
- MUST NOT log decrypted PII.
|
|
71
|
+
- MUST NOT emit PII to analytics pipelines.
|
|
72
|
+
- MUST implement structured redaction for logs and error reporting.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Architecture: Table Separation
|
|
77
|
+
|
|
78
|
+
### What belongs in the business table
|
|
79
|
+
|
|
80
|
+
Keep only non-sensitive business state:
|
|
81
|
+
|
|
82
|
+
- lifecycle status, steps
|
|
83
|
+
- timestamps
|
|
84
|
+
- risk flags / derived fields (non-PII)
|
|
85
|
+
- foreign keys / references
|
|
86
|
+
|
|
87
|
+
### What belongs in the PII table
|
|
88
|
+
|
|
89
|
+
- encrypted payload (ciphertext)
|
|
90
|
+
- encrypted DEK
|
|
91
|
+
- key metadata (key id, versions)
|
|
92
|
+
- schema version
|
|
93
|
+
- timestamps
|
|
94
|
+
|
|
95
|
+
### Draft vs submitted data
|
|
96
|
+
|
|
97
|
+
If your workflow has drafts:
|
|
98
|
+
|
|
99
|
+
- MUST keep draft PII encrypted the same way as submitted PII.
|
|
100
|
+
- MUST NOT keep draft PII in general draft JSON.
|
|
101
|
+
|
|
102
|
+
Practical approach:
|
|
103
|
+
|
|
104
|
+
- `applications.draftData` -> non-PII draft fields only
|
|
105
|
+
- `application_pii` -> encrypted PII payload including draft PII
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Required Postgres Schema
|
|
110
|
+
|
|
111
|
+
### Recommended DDL (bytea for ciphertext)
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
CREATE TABLE application_pii (
|
|
115
|
+
application_id uuid PRIMARY KEY REFERENCES applications(id) ON DELETE CASCADE,
|
|
116
|
+
|
|
117
|
+
-- Wrapped DEK (encrypted with KEK) - required for envelope encryption
|
|
118
|
+
dek_ciphertext bytea NOT NULL,
|
|
119
|
+
|
|
120
|
+
-- PII payload encrypted with DEK (AES-256-GCM)
|
|
121
|
+
pii_ciphertext bytea NOT NULL,
|
|
122
|
+
|
|
123
|
+
-- Key metadata
|
|
124
|
+
kek_key_id text NOT NULL, -- e.g. "pii-kek-v3"
|
|
125
|
+
schema_version int NOT NULL DEFAULT 1,
|
|
126
|
+
|
|
127
|
+
-- Optional: integrity/ops metadata
|
|
128
|
+
pii_hash text NULL, -- hash of plaintext (for change detection) - do NOT use for lookups
|
|
129
|
+
last_decrypted_at timestamptz NULL, -- optional, for ops; do not over-collect
|
|
130
|
+
|
|
131
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
132
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
CREATE INDEX application_pii_kek_key_id_idx ON application_pii(kek_key_id);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Storage format requirements
|
|
139
|
+
|
|
140
|
+
- `dek_ciphertext` stores the wrapped DEK returned by KMS (opaque bytes).
|
|
141
|
+
- `pii_ciphertext` stores a versioned AEAD blob described below.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Ciphertext Format (PII Payload)
|
|
146
|
+
|
|
147
|
+
### PII payload encryption format (recommended)
|
|
148
|
+
|
|
149
|
+
Store as `bytea` with explicit layout:
|
|
150
|
+
|
|
151
|
+
- 1 byte: format version (currently `0x01`)
|
|
152
|
+
- 12 bytes: IV/nonce (random per encryption)
|
|
153
|
+
- 16 bytes: auth tag (GCM)
|
|
154
|
+
- N bytes: ciphertext
|
|
155
|
+
|
|
156
|
+
Layout:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
[ v1 ][ iv (12) ][ tag (16) ][ ciphertext (N) ]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Why:
|
|
163
|
+
|
|
164
|
+
- easy decoding
|
|
165
|
+
- supports future algorithm changes via the format version byte
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## AAD (Associated Authenticated Data)
|
|
170
|
+
|
|
171
|
+
### AAD MUST include
|
|
172
|
+
|
|
173
|
+
Bind ciphertext to its intended record to prevent blob swapping:
|
|
174
|
+
|
|
175
|
+
- `application_id`
|
|
176
|
+
- `schema_version`
|
|
177
|
+
|
|
178
|
+
Recommended AAD string:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
application_pii:{application_id}:{schema_version}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
If a ciphertext blob is copied to another `application_id`, decryption must fail authentication.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Envelope Encryption Flow (Standard)
|
|
189
|
+
|
|
190
|
+
### Encrypt (write/update PII)
|
|
191
|
+
|
|
192
|
+
1. Validate PII object against schema (zod).
|
|
193
|
+
2. Serialize to JSON bytes.
|
|
194
|
+
3. Generate random DEK (32 bytes).
|
|
195
|
+
4. Encrypt JSON bytes with DEK using AES-256-GCM (with AAD).
|
|
196
|
+
5. Wrap DEK using KEK via KMS (returns `dek_ciphertext`).
|
|
197
|
+
6. Store `dek_ciphertext`, `pii_ciphertext`, `kek_key_id`, `schema_version`.
|
|
198
|
+
|
|
199
|
+
### Decrypt (read PII)
|
|
200
|
+
|
|
201
|
+
1. Load `dek_ciphertext`, `pii_ciphertext`, `kek_key_id`, `schema_version`.
|
|
202
|
+
2. Unwrap DEK using KMS + `kek_key_id`.
|
|
203
|
+
3. Decrypt `pii_ciphertext` using DEK and computed AAD.
|
|
204
|
+
4. Parse JSON.
|
|
205
|
+
5. Validate decrypted payload against schema version.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Prisma Models (Production Baseline)
|
|
210
|
+
|
|
211
|
+
```prisma
|
|
212
|
+
model Application {
|
|
213
|
+
id String @id @default(uuid()) @db.Uuid
|
|
214
|
+
userId String @db.Uuid
|
|
215
|
+
|
|
216
|
+
status ApplicationStatus @default(DRAFT)
|
|
217
|
+
currentStep String @default("start")
|
|
218
|
+
draftData Json @default("{}") // MUST be non-PII only
|
|
219
|
+
|
|
220
|
+
version Int @default(1)
|
|
221
|
+
|
|
222
|
+
createdAt DateTime @default(now())
|
|
223
|
+
updatedAt DateTime @updatedAt
|
|
224
|
+
|
|
225
|
+
pii ApplicationPII?
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
model ApplicationPII {
|
|
229
|
+
applicationId String @id @db.Uuid
|
|
230
|
+
application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
|
231
|
+
|
|
232
|
+
dekCiphertext Bytes
|
|
233
|
+
piiCiphertext Bytes
|
|
234
|
+
|
|
235
|
+
kekKeyId String
|
|
236
|
+
schemaVersion Int @default(1)
|
|
237
|
+
|
|
238
|
+
piiHash String?
|
|
239
|
+
createdAt DateTime @default(now())
|
|
240
|
+
updatedAt DateTime @updatedAt
|
|
241
|
+
|
|
242
|
+
@@index([kekKeyId])
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
enum ApplicationStatus {
|
|
246
|
+
DRAFT
|
|
247
|
+
SUBMITTED
|
|
248
|
+
PROCESSING
|
|
249
|
+
AWAITING_EXTERNAL
|
|
250
|
+
AWAITING_REVIEW
|
|
251
|
+
APPROVED
|
|
252
|
+
REJECTED
|
|
253
|
+
FAILED
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Reference Implementation (TypeScript)
|
|
260
|
+
|
|
261
|
+
### PII Schema (zod)
|
|
262
|
+
|
|
263
|
+
Version your schema explicitly.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { z } from "zod";
|
|
267
|
+
|
|
268
|
+
export const PiiV1 = z.object({
|
|
269
|
+
fullName: z.string().min(1),
|
|
270
|
+
dob: z.string().min(4), // prefer ISO date string; validate more strictly in real code
|
|
271
|
+
address: z.object({
|
|
272
|
+
line1: z.string().min(1),
|
|
273
|
+
suburb: z.string().min(1),
|
|
274
|
+
postcode: z.string().min(3),
|
|
275
|
+
country: z.string().min(2),
|
|
276
|
+
}),
|
|
277
|
+
email: z.string().email().optional(),
|
|
278
|
+
phone: z.string().optional(),
|
|
279
|
+
governmentId: z.string().optional(),
|
|
280
|
+
bank: z
|
|
281
|
+
.object({
|
|
282
|
+
bsb: z.string().optional(),
|
|
283
|
+
accountNumber: z.string().optional(),
|
|
284
|
+
})
|
|
285
|
+
.optional(),
|
|
286
|
+
});
|
|
287
|
+
export type PiiV1Type = z.infer<typeof PiiV1>;
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Crypto helpers (AES-256-GCM)
|
|
291
|
+
|
|
292
|
+
Use Node `crypto` for AEAD. Keep it isolated in a module.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import crypto from "crypto";
|
|
296
|
+
|
|
297
|
+
const IV_LEN = 12;
|
|
298
|
+
const TAG_LEN = 16;
|
|
299
|
+
const FORMAT_V1 = 0x01;
|
|
300
|
+
|
|
301
|
+
export function encryptAesGcm(params: {
|
|
302
|
+
key: Buffer; // 32 bytes
|
|
303
|
+
plaintext: Buffer;
|
|
304
|
+
aad: Buffer;
|
|
305
|
+
}): Buffer {
|
|
306
|
+
if (params.key.length !== 32) throw new Error("DEK must be 32 bytes");
|
|
307
|
+
const iv = crypto.randomBytes(IV_LEN);
|
|
308
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", params.key, iv);
|
|
309
|
+
cipher.setAAD(params.aad);
|
|
310
|
+
const ciphertext = Buffer.concat([cipher.update(params.plaintext), cipher.final()]);
|
|
311
|
+
const tag = cipher.getAuthTag();
|
|
312
|
+
|
|
313
|
+
return Buffer.concat([Buffer.from([FORMAT_V1]), iv, tag, ciphertext]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function decryptAesGcm(params: {
|
|
317
|
+
key: Buffer;
|
|
318
|
+
blob: Buffer;
|
|
319
|
+
aad: Buffer;
|
|
320
|
+
}): Buffer {
|
|
321
|
+
const version = params.blob.readUInt8(0);
|
|
322
|
+
if (version !== FORMAT_V1) throw new Error(`Unsupported ciphertext version: ${version}`);
|
|
323
|
+
|
|
324
|
+
const iv = params.blob.subarray(1, 1 + IV_LEN);
|
|
325
|
+
const tag = params.blob.subarray(1 + IV_LEN, 1 + IV_LEN + TAG_LEN);
|
|
326
|
+
const ciphertext = params.blob.subarray(1 + IV_LEN + TAG_LEN);
|
|
327
|
+
|
|
328
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", params.key, iv);
|
|
329
|
+
decipher.setAAD(params.aad);
|
|
330
|
+
decipher.setAuthTag(tag);
|
|
331
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### KMS interface (KEK wrap/unwrap)
|
|
336
|
+
|
|
337
|
+
You will integrate a KMS provider (AWS KMS, Azure Key Vault, GCP KMS). Keep it behind an interface.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
export interface KekProvider {
|
|
341
|
+
/** Wraps (encrypts) a DEK under the specified KEK key version. */
|
|
342
|
+
wrapDek(params: { kekKeyId: string; dek: Buffer }): Promise<Buffer>;
|
|
343
|
+
/** Unwraps (decrypts) a wrapped DEK using the specified KEK key version. */
|
|
344
|
+
unwrapDek(params: { kekKeyId: string; wrappedDek: Buffer }): Promise<Buffer>;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### End-to-end encrypt/decrypt
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { PiiV1 } from "./piiSchema";
|
|
352
|
+
import { encryptAesGcm, decryptAesGcm } from "./crypto";
|
|
353
|
+
import crypto from "crypto";
|
|
354
|
+
|
|
355
|
+
function aadFor(applicationId: string, schemaVersion: number): Buffer {
|
|
356
|
+
return Buffer.from(`application_pii:${applicationId}:${schemaVersion}`, "utf8");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function encryptPiiRecord(params: {
|
|
360
|
+
applicationId: string;
|
|
361
|
+
schemaVersion: number;
|
|
362
|
+
kekKeyId: string;
|
|
363
|
+
pii: unknown;
|
|
364
|
+
kek: KekProvider;
|
|
365
|
+
}): Promise<{ dekCiphertext: Buffer; piiCiphertext: Buffer; piiHash: string }> {
|
|
366
|
+
// Validate
|
|
367
|
+
const parsed = PiiV1.parse(params.pii);
|
|
368
|
+
|
|
369
|
+
const plaintext = Buffer.from(JSON.stringify(parsed), "utf8");
|
|
370
|
+
const dek = crypto.randomBytes(32);
|
|
371
|
+
|
|
372
|
+
const piiCiphertext = encryptAesGcm({
|
|
373
|
+
key: dek,
|
|
374
|
+
plaintext,
|
|
375
|
+
aad: aadFor(params.applicationId, params.schemaVersion),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const dekCiphertext = await params.kek.wrapDek({ kekKeyId: params.kekKeyId, dek });
|
|
379
|
+
|
|
380
|
+
// Optional: hash for change detection (NOT for searching)
|
|
381
|
+
const piiHash = crypto.createHash("sha256").update(plaintext).digest("hex");
|
|
382
|
+
|
|
383
|
+
return { dekCiphertext, piiCiphertext, piiHash };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function decryptPiiRecord(params: {
|
|
387
|
+
applicationId: string;
|
|
388
|
+
schemaVersion: number;
|
|
389
|
+
kekKeyId: string;
|
|
390
|
+
dekCiphertext: Buffer;
|
|
391
|
+
piiCiphertext: Buffer;
|
|
392
|
+
kek: KekProvider;
|
|
393
|
+
}): Promise<unknown> {
|
|
394
|
+
const dek = await params.kek.unwrapDek({
|
|
395
|
+
kekKeyId: params.kekKeyId,
|
|
396
|
+
wrappedDek: params.dekCiphertext,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const plaintext = decryptAesGcm({
|
|
400
|
+
key: dek,
|
|
401
|
+
blob: params.piiCiphertext,
|
|
402
|
+
aad: aadFor(params.applicationId, params.schemaVersion),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const obj = JSON.parse(plaintext.toString("utf8"));
|
|
406
|
+
|
|
407
|
+
// Validate against schema version (example uses v1)
|
|
408
|
+
return PiiV1.parse(obj);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Prisma Usage Patterns
|
|
415
|
+
|
|
416
|
+
### Write/update PII (upsert)
|
|
417
|
+
|
|
418
|
+
- Encrypt in application code.
|
|
419
|
+
- Store ciphertext + wrapped DEK.
|
|
420
|
+
- Never store plaintext at rest.
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
await prisma.applicationPII.upsert({
|
|
424
|
+
where: { applicationId },
|
|
425
|
+
create: {
|
|
426
|
+
applicationId,
|
|
427
|
+
dekCiphertext,
|
|
428
|
+
piiCiphertext,
|
|
429
|
+
kekKeyId,
|
|
430
|
+
schemaVersion,
|
|
431
|
+
piiHash,
|
|
432
|
+
},
|
|
433
|
+
update: {
|
|
434
|
+
dekCiphertext,
|
|
435
|
+
piiCiphertext,
|
|
436
|
+
kekKeyId,
|
|
437
|
+
schemaVersion,
|
|
438
|
+
piiHash,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Read PII (explicit, controlled)
|
|
444
|
+
|
|
445
|
+
Avoid `include: { pii: true }` as a default. Only include PII in carefully scoped code paths.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const row = await prisma.applicationPII.findUnique({ where: { applicationId } });
|
|
449
|
+
if (!row) return null;
|
|
450
|
+
|
|
451
|
+
const pii = await decryptPiiRecord({
|
|
452
|
+
applicationId,
|
|
453
|
+
schemaVersion: row.schemaVersion,
|
|
454
|
+
kekKeyId: row.kekKeyId,
|
|
455
|
+
dekCiphertext: row.dekCiphertext,
|
|
456
|
+
piiCiphertext: row.piiCiphertext,
|
|
457
|
+
kek,
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Key Rotation (KEK Rotation)
|
|
464
|
+
|
|
465
|
+
### Requirements
|
|
466
|
+
|
|
467
|
+
- MUST support decrypt with old and new KEK key versions.
|
|
468
|
+
- MUST encrypt new writes with the latest KEK version.
|
|
469
|
+
- MUST provide a migration plan to re-wrap DEKs and (optionally) re-encrypt payloads.
|
|
470
|
+
|
|
471
|
+
### Standard rotation phases
|
|
472
|
+
|
|
473
|
+
1. Introduce new KEK (e.g. v4) in KMS.
|
|
474
|
+
2. Deploy app that:
|
|
475
|
+
- wraps new DEKs under v4
|
|
476
|
+
- unwraps DEKs under v1..v4 as needed
|
|
477
|
+
3. Migrate existing rows.
|
|
478
|
+
|
|
479
|
+
#### Migration Option A: Re-wrap DEK only (preferred)
|
|
480
|
+
|
|
481
|
+
- unwrap DEK using old KEK
|
|
482
|
+
- wrap same DEK with new KEK
|
|
483
|
+
- update `dek_ciphertext`, `kek_key_id`
|
|
484
|
+
- no need to re-encrypt `pii_ciphertext` (payload stays encrypted under DEK)
|
|
485
|
+
|
|
486
|
+
#### Migration Option B: Full re-encrypt (rare)
|
|
487
|
+
|
|
488
|
+
- decrypt payload
|
|
489
|
+
- generate new DEK
|
|
490
|
+
- encrypt payload
|
|
491
|
+
- wrap new DEK
|
|
492
|
+
|
|
493
|
+
Use only if you suspect DEK exposure or require periodic payload re-encryption.
|
|
494
|
+
|
|
495
|
+
### Rotation completeness check
|
|
496
|
+
|
|
497
|
+
- query: `SELECT count(*) FROM application_pii WHERE kek_key_id != 'pii-kek-v4';`
|
|
498
|
+
- must reach 0 before retiring v1..v3
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Failure Modes & Required Behavior
|
|
503
|
+
|
|
504
|
+
### Decryption fails (auth tag / AAD mismatch)
|
|
505
|
+
|
|
506
|
+
Likely causes:
|
|
507
|
+
|
|
508
|
+
- wrong application id/AAD
|
|
509
|
+
- ciphertext corruption
|
|
510
|
+
- tampering
|
|
511
|
+
|
|
512
|
+
Required behavior:
|
|
513
|
+
|
|
514
|
+
- treat as security incident signal
|
|
515
|
+
- return safe error (no payload leakage)
|
|
516
|
+
- emit security event (not containing PII)
|
|
517
|
+
|
|
518
|
+
### KMS unwrap fails
|
|
519
|
+
|
|
520
|
+
Likely causes:
|
|
521
|
+
|
|
522
|
+
- missing permissions
|
|
523
|
+
- wrong key id
|
|
524
|
+
- key disabled/rotated incorrectly
|
|
525
|
+
|
|
526
|
+
Required behavior:
|
|
527
|
+
|
|
528
|
+
- fail closed (do not proceed)
|
|
529
|
+
- alert immediately
|
|
530
|
+
- degrade endpoints that require PII
|
|
531
|
+
|
|
532
|
+
### Schema validation fails after decryption
|
|
533
|
+
|
|
534
|
+
Likely causes:
|
|
535
|
+
|
|
536
|
+
- schema drift
|
|
537
|
+
- historical bad data
|
|
538
|
+
|
|
539
|
+
Required behavior:
|
|
540
|
+
|
|
541
|
+
- surface a controlled "data format invalid" error
|
|
542
|
+
- provide migration path
|
|
543
|
+
- log only metadata (schemaVersion, appId), never the plaintext
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Query & Search Guidance
|
|
548
|
+
|
|
549
|
+
### You cannot query encrypted PII
|
|
550
|
+
|
|
551
|
+
Accept this. Do not design SQL queries that need plaintext PII.
|
|
552
|
+
|
|
553
|
+
### Allowed patterns
|
|
554
|
+
|
|
555
|
+
- Store derived, non-sensitive fields outside encryption:
|
|
556
|
+
- `country_code`, `age_band`, `risk_flag`
|
|
557
|
+
- Store hashed lookup tokens for controlled dedupe:
|
|
558
|
+
- `email_hash = sha256(lowercase(email))` (still sensitive; treat as restricted)
|
|
559
|
+
|
|
560
|
+
If you add hash fields:
|
|
561
|
+
|
|
562
|
+
- keep them out of analytics
|
|
563
|
+
- restrict access like PII
|
|
564
|
+
- document their purpose
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Tests (Minimum)
|
|
569
|
+
|
|
570
|
+
### Crypto correctness tests
|
|
571
|
+
|
|
572
|
+
- encrypt->decrypt roundtrip
|
|
573
|
+
- AAD mismatch fails decrypt
|
|
574
|
+
- swapped ciphertext across ids fails decrypt
|
|
575
|
+
- corrupted blob fails decrypt
|
|
576
|
+
- version byte unsupported fails decrypt
|
|
577
|
+
|
|
578
|
+
### Storage tests
|
|
579
|
+
|
|
580
|
+
- PII never present in business table after writes
|
|
581
|
+
- logs redact/omit PII
|
|
582
|
+
|
|
583
|
+
### Rotation tests
|
|
584
|
+
|
|
585
|
+
- decrypt old key id works
|
|
586
|
+
- re-wrap migration updates `kek_key_id` and keeps data readable
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## PR Review Checklist (Enforcement)
|
|
591
|
+
|
|
592
|
+
- [ ] No PII fields added to business tables.
|
|
593
|
+
- [ ] No PII added to `draftData` JSON.
|
|
594
|
+
- [ ] Encryption uses AES-256-GCM with random IV.
|
|
595
|
+
- [ ] AAD includes record identity + schema version.
|
|
596
|
+
- [ ] `kek_key_id` and `schema_version` stored.
|
|
597
|
+
- [ ] No decrypted PII logged or sent to analytics.
|
|
598
|
+
- [ ] Rotation path documented for any key change.
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Anti-Patterns (MUST NOT)
|
|
603
|
+
|
|
604
|
+
- Storing plaintext PII in DB "temporarily"
|
|
605
|
+
- Using DB-native encryption only (without app-layer envelope encryption)
|
|
606
|
+
- Deterministic encryption for general PII
|
|
607
|
+
- Reusing IVs
|
|
608
|
+
- Omitting AAD
|
|
609
|
+
- Storing keys in DB or code
|
|
610
|
+
- Returning decrypted PII via broad "include" queries by default
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Notes on DB Permissions with Prisma
|
|
615
|
+
|
|
616
|
+
Prisma commonly uses one DB user. Table-level enforcement requires:
|
|
617
|
+
|
|
618
|
+
- separate DB roles + separate Prisma clients, or
|
|
619
|
+
- a service boundary where only a dedicated service can access `application_pii`
|
|
620
|
+
|
|
621
|
+
If you cannot enforce via DB roles today:
|
|
622
|
+
|
|
623
|
+
- enforce via strict repository modules + code review
|
|
624
|
+
- add automated lint checks for forbidden includes
|
|
625
|
+
- plan a path to split DB roles later
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
End of Skill.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: process-metrics
|
|
3
|
+
description: Define a portable metrics schema and templates for evaluating workflow performance. Use after /workflow:work, /workflow:review, or /workflow:compound to log outcomes and refine the system.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Process Metrics
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Measure workflow performance so the system can improve over time.
|
|
11
|
+
|
|
12
|
+
Metrics should be:
|
|
13
|
+
|
|
14
|
+
- light-weight (takes < 3 minutes)
|
|
15
|
+
- comparable (same fields each time)
|
|
16
|
+
- actionable (each failure maps to an improvement)
|
|
17
|
+
|
|
18
|
+
## Storage
|
|
19
|
+
|
|
20
|
+
- Daily: `docs/metrics/daily/YYYY-MM-DD.md`
|
|
21
|
+
- Weekly: `docs/metrics/weekly/YYYY-WW.md`
|
|
22
|
+
- Monthly: `docs/metrics/monthly/YYYY-MM.md`
|
|
23
|
+
|
|
24
|
+
## Core Fields
|
|
25
|
+
|
|
26
|
+
- workflow: brainstorm|plan|work|triage|review|compound|test-browser|other
|
|
27
|
+
- context: plan/todo/pr/solution path or label
|
|
28
|
+
- outcome: success|partial|failed
|
|
29
|
+
- minutes: integer
|
|
30
|
+
- risk_tier: low|medium|high
|
|
31
|
+
- quality:
|
|
32
|
+
- tests: ran|skipped|failed
|
|
33
|
+
- lint: ran|skipped|failed
|
|
34
|
+
- blocker: one sentence
|
|
35
|
+
- rework: 0|1|2|3+ (how many times you had to redo work)
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- Failures should include "what failed" + "why" + "what to change".
|
|
40
|
+
- Improvements should target a component: command/skill/agent/config.
|
|
41
|
+
|
|
42
|
+
## Templates
|
|
43
|
+
|
|
44
|
+
- [daily-template.md](./assets/daily-template.md)
|
|
45
|
+
- [weekly-template.md](./assets/weekly-template.md)
|
|
46
|
+
- [monthly-template.md](./assets/monthly-template.md)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: YYYY-MM-DD
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Daily Metrics
|
|
6
|
+
|
|
7
|
+
## Session
|
|
8
|
+
|
|
9
|
+
- workflow: <brainstorm|plan|work|triage|review|compound|test-browser|other>
|
|
10
|
+
- context: <path or short label>
|
|
11
|
+
- outcome: <success|partial|failed>
|
|
12
|
+
- minutes: <int>
|
|
13
|
+
- risk_tier: <low|medium|high>
|
|
14
|
+
|
|
15
|
+
## Quality Signals
|
|
16
|
+
|
|
17
|
+
- tests: <ran|skipped|failed>
|
|
18
|
+
- lint: <ran|skipped|failed>
|
|
19
|
+
|
|
20
|
+
## Friction
|
|
21
|
+
|
|
22
|
+
- blocker: <one sentence>
|
|
23
|
+
- rework: <0|1|2|3+>
|
|
24
|
+
|
|
25
|
+
## What Failed (if any)
|
|
26
|
+
|
|
27
|
+
- <bullet>
|
|
28
|
+
|
|
29
|
+
## Assessment
|
|
30
|
+
|
|
31
|
+
- root cause: <one sentence>
|
|
32
|
+
- what to change next time: <1-3 bullets>
|
|
33
|
+
- component targets: <command|skill|agent|config|todo>
|
|
34
|
+
|
|
35
|
+
## Actions
|
|
36
|
+
|
|
37
|
+
- <action> (target: command|skill|agent|config)
|