@punica/editor 1.16.0 → 1.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punica/editor",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Punica Editor",
5
5
  "private": false,
6
6
  "type": "module",
@@ -175,6 +175,12 @@ declare module 'punica' {
175
175
  requiresApproval: boolean;
176
176
  suggestedScope?: ApprovalScope;
177
177
  reason?: string;
178
+ /**
179
+ * Set when a rule from a policy template decided this. Absent
180
+ * means the decision came from the capability's own declaration,
181
+ * which is the state of every install with no `.punica/policy.yaml`.
182
+ */
183
+ policyRule?: PolicyRuleRef;
178
184
  }
179
185
 
180
186
  export interface PolicyApi {
@@ -183,7 +189,32 @@ declare module 'punica' {
183
189
 
184
190
  evaluate(
185
191
  req: PolicyRequest,
186
- ctx?: { workspaceId?: string }
192
+ ctx?: {
193
+ workspaceId?: string;
194
+ /**
195
+ * The REDACTED arguments, for rule conditions to test. The
196
+ * same value `argsBinding` is computed from, so a condition
197
+ * can never read a secret and the decision, the binding and
198
+ * the audit record's `input` all agree.
199
+ *
200
+ * Used transiently for matching and never persisted — only the
201
+ * digest and the capped preview are.
202
+ */
203
+ args?: unknown;
204
+ /**
205
+ * Input paths the redactor has already replaced. A condition
206
+ * on one of them never matches — comparing a bucket name
207
+ * against `'[REDACTED]'` would be governance in appearance
208
+ * only.
209
+ */
210
+ redactedPaths?: ReadonlyArray<ReadonlyArray<string>>;
211
+ /**
212
+ * Every string in `args` was replaced, not selected paths —
213
+ * what the redactor does for a `secretBearing` capability. No
214
+ * condition can mean anything against that, so none apply.
215
+ */
216
+ argsWhollyRedacted?: boolean;
217
+ }
187
218
  ): PolicyDecision;
188
219
 
189
220
  requestApproval(
@@ -278,31 +309,150 @@ declare module 'punica' {
278
309
  }
279
310
 
280
311
  /**
281
- * Policy template for workspace-scoped policy rules
312
+ * What a matching rule does to the decision.
313
+ *
314
+ * - `deny` — the call does not run, and no approval can make it
315
+ * run. Terminal by design: a rule that a prompt could
316
+ * click past is a suggestion, not a policy.
317
+ * - `require` — the call needs approval even if the capability
318
+ * declares `approval: none`.
319
+ * - `allow` — the call runs without a prompt. Attributed by name
320
+ * in the decision and in the durable audit record, so
321
+ * a later reader can tell "a rule allowed this" from
322
+ * "a human approved this".
323
+ */
324
+ export type PolicyRuleEffect = 'allow' | 'deny' | 'require';
325
+
326
+ /**
327
+ * Comparison operators a rule condition may use.
328
+ *
329
+ * A deliberately closed set. Every entry has an obvious equivalent
330
+ * in the gateway policy schemas this vocabulary is meant to be
331
+ * exported into (K4) — regex is absent for exactly that reason: it
332
+ * cannot cross into a foreign engine without either changing meaning
333
+ * or carrying a ReDoS hazard with it.
334
+ */
335
+ export type PolicyConditionOp =
336
+ | 'eq'
337
+ | 'neq'
338
+ | 'in'
339
+ | 'notIn'
340
+ | 'startsWith'
341
+ | 'endsWith'
342
+ | 'glob'
343
+ | 'exists'
344
+ | 'absent'
345
+ | 'lt'
346
+ | 'lte'
347
+ | 'gt'
348
+ | 'gte';
349
+
350
+ /**
351
+ * One test against one argument path.
352
+ *
353
+ * `path` is dotted, with `*` for every element of an array
354
+ * (`bucket`, `target.path`, `items.*.id`) — the same grammar as the
355
+ * `policyKey` annotation, so there is one path syntax to learn.
356
+ *
357
+ * The value tested is the REDACTED argument, the same one the
358
+ * binding is computed from. A condition on a `sensitive` field
359
+ * therefore cannot match anything meaningful and never matches at
360
+ * all; the capability linter reports the declaration side of that
361
+ * contradiction.
362
+ */
363
+ export interface PolicyCondition {
364
+ path: string;
365
+ op: PolicyConditionOp;
366
+ /** Not read by `exists` / `absent`. */
367
+ value?: unknown;
368
+ }
369
+
370
+ /**
371
+ * One rule. `kind`/`id` select which requests it applies to; `when`
372
+ * selects which argument tuples.
373
+ */
374
+ export interface PolicyRule {
375
+ /** Request kind, e.g. `capability`. */
376
+ kind: string;
377
+ /** Exact id, or a trailing-`*` prefix glob. Absent matches every id of the kind. */
378
+ id?: string;
379
+ effect: PolicyRuleEffect;
380
+ /** ANDed. Absent or empty matches every argument tuple. */
381
+ when?: PolicyCondition[];
382
+ /** Which approval `effect: 'require'` demands. Default `step`. */
383
+ approval?: 'none' | 'plan' | 'step';
384
+ /** Shown to the user on a denial and written into the audit record. */
385
+ reason?: string;
386
+ }
387
+
388
+ /** Which rule decided, carried on the decision and into the audit record. */
389
+ export interface PolicyRuleRef {
390
+ /** Name of the template the rule came from. */
391
+ template: string;
392
+ /** Index within that template's `rules`. */
393
+ ruleIndex: number;
394
+ effect: PolicyRuleEffect;
395
+ reason?: string;
396
+ /** Digest of the template's rules — which policy text was in force. */
397
+ digest?: string;
398
+ }
399
+
400
+ /** A rule that matched, plus the rule itself for the caller to read. */
401
+ export interface PolicyRuleMatch {
402
+ ref: PolicyRuleRef;
403
+ rule: PolicyRule;
404
+ }
405
+
406
+ /** One problem found while validating rule input. */
407
+ export interface PolicyRuleProblem {
408
+ /** Dotted path into the parsed input, e.g. `templates.0.rules.2.op`. */
409
+ path: string;
410
+ message: string;
411
+ }
412
+
413
+ /**
414
+ * A named, workspace-scoped bundle of rules.
415
+ *
416
+ * Until 1.17.0 this type existed but nothing read it: `evaluate()`
417
+ * never consulted the template store, `#persist()` never saved it,
418
+ * and its `condition?: string` field was marked "Future" and read by
419
+ * nobody. The string expression is gone — a structured matcher is
420
+ * the half that can be translated into another engine's schema,
421
+ * which is the entire purpose of exporting policy at all.
282
422
  */
283
423
  export interface PolicyTemplate {
284
424
  name: string;
285
425
  description?: string;
286
426
  workspaceId?: string;
287
- rules: Array<{
288
- kind: string;
289
- id?: string;
290
- risk?: 'low' | 'medium' | 'high';
291
- approval?: 'none' | 'plan' | 'step';
292
- condition?: string; // Future: expression for conditional rules
293
- }>;
427
+ rules: PolicyRule[];
428
+ /**
429
+ * Where the template came from. `file` templates are owned by
430
+ * `.punica/policy.yaml` and are removed when they leave it, so
431
+ * deleting the file cannot leave stale rules enforcing from
432
+ * persisted state. Absent is treated as `api`.
433
+ */
434
+ source?: 'file' | 'api';
435
+ /** Digest of `rules`, set by whoever loaded them. */
436
+ digest?: string;
294
437
  createdAtMs?: number;
295
438
  updatedAtMs?: number;
296
439
  }
297
440
 
298
441
  /**
299
- * Exported policy state format (v2).
442
+ * Exported policy state.
443
+ *
444
+ * v3 adds `templates`, so an export answers "what was in force"
445
+ * rather than only "what was approved" — the question an evidence
446
+ * package has to answer. `importState` accepts v2 as well: a v2 blob
447
+ * simply carries no rules.
300
448
  */
301
449
  export interface PolicyExport {
302
- version: 2;
450
+ version: 2 | 3;
303
451
  exportedAt: number;
304
452
  mode: PolicyMode;
305
453
  approvals: ApprovalRecord[];
454
+ /** Present in v3. Absent in a v2 blob. */
455
+ templates?: PolicyTemplate[];
306
456
  }
307
457
 
308
458
  /**
@@ -382,6 +532,50 @@ declare module 'punica' {
382
532
  source: unknown,
383
533
  options?: { policyKeyPaths?: ReadonlyArray<ReadonlyArray<string>> }
384
534
  ): Promise<ArgsBinding>;
535
+
536
+ /**
537
+ * SHA-256 over the canonical JSON of `value`, 128 bits of hex.
538
+ * Used for the rule-set digest that says which policy text was in
539
+ * force; shares its implementation with `computeArgsBinding` so
540
+ * the two cannot drift.
541
+ */
542
+ function canonicalDigest(value: unknown): Promise<string>;
543
+
544
+ /**
545
+ * Which rule applies to a request, if any.
546
+ *
547
+ * Precedence is `deny` > `require` > `allow`, independent of the
548
+ * order rules were written in — order-dependent precedence does
549
+ * not survive translation into another policy engine.
550
+ *
551
+ * `args` must be the REDACTED arguments. Exported so a policy
552
+ * exporter reads the same matcher the gate enforces instead of
553
+ * reimplementing it.
554
+ */
555
+ function matchRules(
556
+ templates: readonly PolicyTemplate[],
557
+ req: { kind: string; id: string },
558
+ args: unknown,
559
+ options?: {
560
+ /** Paths the redactor has already replaced; conditions on them never match. */
561
+ sensitivePaths?: ReadonlyArray<ReadonlyArray<string>>;
562
+ /** The whole tuple was replaced (a `secretBearing` capability); no condition applies. */
563
+ argsWhollyRedacted?: boolean;
564
+ }
565
+ ): PolicyRuleMatch | null;
566
+
567
+ /**
568
+ * Normalise and check rule input, e.g. parsed
569
+ * `.punica/policy.yaml`.
570
+ *
571
+ * An invalid rule is dropped and reported while its siblings load;
572
+ * a file-level shape error yields zero templates and a problem.
573
+ * Never returns a partial set that reads as complete.
574
+ */
575
+ function validatePolicyTemplates(raw: unknown): {
576
+ templates: PolicyTemplate[];
577
+ problems: PolicyRuleProblem[];
578
+ };
385
579
  }
386
580
  }
387
581
  }