@qmilab/lodestar-core 0.1.5 → 0.2.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 (56) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schemas/action.d.ts +31 -13
  6. package/dist/schemas/action.d.ts.map +1 -1
  7. package/dist/schemas/action.js +20 -1
  8. package/dist/schemas/action.js.map +1 -1
  9. package/dist/schemas/approval.d.ts +271 -0
  10. package/dist/schemas/approval.d.ts.map +1 -0
  11. package/dist/schemas/approval.js +119 -0
  12. package/dist/schemas/approval.js.map +1 -0
  13. package/dist/schemas/belief.d.ts.map +1 -1
  14. package/dist/schemas/belief.js +7 -1
  15. package/dist/schemas/belief.js.map +1 -1
  16. package/dist/schemas/calibration.d.ts +977 -0
  17. package/dist/schemas/calibration.d.ts.map +1 -0
  18. package/dist/schemas/calibration.js +187 -0
  19. package/dist/schemas/calibration.js.map +1 -0
  20. package/dist/schemas/claim.d.ts.map +1 -1
  21. package/dist/schemas/claim.js +4 -2
  22. package/dist/schemas/claim.js.map +1 -1
  23. package/dist/schemas/common.d.ts.map +1 -1
  24. package/dist/schemas/common.js +11 -5
  25. package/dist/schemas/common.js.map +1 -1
  26. package/dist/schemas/policy.d.ts +768 -0
  27. package/dist/schemas/policy.d.ts.map +1 -0
  28. package/dist/schemas/policy.js +200 -0
  29. package/dist/schemas/policy.js.map +1 -0
  30. package/dist/schemas/probe-pack.d.ts +152 -0
  31. package/dist/schemas/probe-pack.d.ts.map +1 -0
  32. package/dist/schemas/probe-pack.js +140 -0
  33. package/dist/schemas/probe-pack.js.map +1 -0
  34. package/dist/schemas/reflection.d.ts +405 -0
  35. package/dist/schemas/reflection.d.ts.map +1 -0
  36. package/dist/schemas/reflection.js +154 -0
  37. package/dist/schemas/reflection.js.map +1 -0
  38. package/dist/schemas/revision.d.ts.map +1 -1
  39. package/dist/schemas/revision.js.map +1 -1
  40. package/dist/schemas/sentinel.d.ts +134 -0
  41. package/dist/schemas/sentinel.d.ts.map +1 -0
  42. package/dist/schemas/sentinel.js +97 -0
  43. package/dist/schemas/sentinel.js.map +1 -0
  44. package/package.json +2 -7
  45. package/src/index.ts +18 -0
  46. package/src/schemas/action.ts +20 -1
  47. package/src/schemas/approval.ts +136 -0
  48. package/src/schemas/belief.ts +7 -1
  49. package/src/schemas/calibration.ts +212 -0
  50. package/src/schemas/claim.ts +15 -8
  51. package/src/schemas/common.ts +16 -10
  52. package/src/schemas/policy.ts +231 -0
  53. package/src/schemas/probe-pack.ts +169 -0
  54. package/src/schemas/reflection.ts +166 -0
  55. package/src/schemas/revision.ts +7 -5
  56. package/src/schemas/sentinel.ts +104 -0
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Sentinel alerts — the wire format a Sentinel emits when it pattern-matches
4
+ * a suspicious shape in the event stream.
5
+ *
6
+ * Design lock: `docs/architecture/sentinels.md` (and Q7 of
7
+ * `docs/architecture/reflection-pass.md`, which settled the execution model).
8
+ * The short version:
9
+ *
10
+ * - Sentinels are an async tail of the event stream. They never block the
11
+ * Action Kernel; they emit `sentinel.alerted@1` events and a future
12
+ * (additive) `arbitrate` hook honours them on the *next* action that
13
+ * depends on a flagged subject.
14
+ * - This is a governance event, not an Observation. Like
15
+ * `reflection.completed@1`, the payload is the event payload directly and
16
+ * is NOT registered in the observation schema registry.
17
+ *
18
+ * Core owns the wire format only. The base class, the runner, and the
19
+ * concrete sentinels live in `@qmilab/lodestar-harness`.
20
+ */
21
+ /**
22
+ * What a sentinel alert is *about*. Kept deliberately small — the four
23
+ * epistemic-chain nouns a v0 sentinel can point at.
24
+ *
25
+ * `belief` is the load-bearing kind for the eventual kernel hook: the
26
+ * `arbitrate` lookup scopes recent alerts to a candidate action's
27
+ * `belief_dependencies`, so a sentinel that names a belief gates the next
28
+ * action that leans on it. `tool_sequence` is a synthetic subject — its
29
+ * `id` identifies the *completion* of a matched sequence (the id of the
30
+ * final action in the run), since no single chain-noun owns the pattern.
31
+ */
32
+ export declare const SentinelSubjectSchema: z.ZodObject<{
33
+ kind: z.ZodEnum<["belief", "action", "decision", "tool_sequence"]>;
34
+ id: z.ZodString;
35
+ }, "strip", z.ZodTypeAny, {
36
+ id: string;
37
+ kind: "belief" | "decision" | "action" | "tool_sequence";
38
+ }, {
39
+ id: string;
40
+ kind: "belief" | "decision" | "action" | "tool_sequence";
41
+ }>;
42
+ export type SentinelSubject = z.infer<typeof SentinelSubjectSchema>;
43
+ /**
44
+ * Triage weight. `critical` is reserved for patterns that, left unflagged,
45
+ * map onto a concrete attack (e.g. the read → external-egress → write
46
+ * exfiltration shape). `warning` is the default for "under-supported but
47
+ * not obviously hostile". `info` is for advisory signal a calibrator may
48
+ * later promote or demote.
49
+ */
50
+ export declare const SentinelSeveritySchema: z.ZodEnum<["info", "warning", "critical"]>;
51
+ export type SentinelSeverity = z.infer<typeof SentinelSeveritySchema>;
52
+ /**
53
+ * The payload of a `sentinel.alerted@1` event.
54
+ *
55
+ * `observed_event_ids` lists exactly the events the sentinel read to reach
56
+ * this conclusion; they are also the alert envelope's `causal_parent_ids`,
57
+ * so `lodestar report` can walk back from an alert to the events that
58
+ * triggered it.
59
+ *
60
+ * `detail` is rule-specific structured context (offending belief ids, the
61
+ * matched tool run, the confidence that tripped the floor, …). It is a
62
+ * record rather than a discriminated union so a new sentinel can ship
63
+ * without a core schema bump; the human-readable `message` is always
64
+ * present so an alert is legible without decoding `detail`.
65
+ *
66
+ * Every field is required (no optionals besides `rationale_id`) so the
67
+ * event-log writer's canonical hash and `JSON.stringify` never disagree on
68
+ * a dropped `undefined` key — the same discipline the firewall audit
69
+ * events follow.
70
+ */
71
+ export declare const SentinelAlertPayloadSchema: z.ZodObject<{
72
+ alert_id: z.ZodString;
73
+ sentinel_name: z.ZodString;
74
+ rule: z.ZodString;
75
+ severity: z.ZodEnum<["info", "warning", "critical"]>;
76
+ subject: z.ZodObject<{
77
+ kind: z.ZodEnum<["belief", "action", "decision", "tool_sequence"]>;
78
+ id: z.ZodString;
79
+ }, "strip", z.ZodTypeAny, {
80
+ id: string;
81
+ kind: "belief" | "decision" | "action" | "tool_sequence";
82
+ }, {
83
+ id: string;
84
+ kind: "belief" | "decision" | "action" | "tool_sequence";
85
+ }>;
86
+ message: z.ZodString;
87
+ observed_event_ids: z.ZodArray<z.ZodString, "many">;
88
+ detail: z.ZodRecord<z.ZodString, z.ZodUnknown>;
89
+ detected_at: z.ZodString;
90
+ /**
91
+ * Reserved. A sentinel may attach a generated Explanation id here, the
92
+ * way reflection proposals carry one. v0 sentinels rely on `message` +
93
+ * `detail` and leave this unset (omitted entirely, not `undefined`, so
94
+ * the canonical hash stays stable).
95
+ */
96
+ rationale_id: z.ZodOptional<z.ZodString>;
97
+ }, "strip", z.ZodTypeAny, {
98
+ message: string;
99
+ subject: {
100
+ id: string;
101
+ kind: "belief" | "decision" | "action" | "tool_sequence";
102
+ };
103
+ detail: Record<string, unknown>;
104
+ observed_event_ids: string[];
105
+ alert_id: string;
106
+ sentinel_name: string;
107
+ rule: string;
108
+ severity: "info" | "warning" | "critical";
109
+ detected_at: string;
110
+ rationale_id?: string | undefined;
111
+ }, {
112
+ message: string;
113
+ subject: {
114
+ id: string;
115
+ kind: "belief" | "decision" | "action" | "tool_sequence";
116
+ };
117
+ detail: Record<string, unknown>;
118
+ observed_event_ids: string[];
119
+ alert_id: string;
120
+ sentinel_name: string;
121
+ rule: string;
122
+ severity: "info" | "warning" | "critical";
123
+ detected_at: string;
124
+ rationale_id?: string | undefined;
125
+ }>;
126
+ export type SentinelAlertPayload = z.infer<typeof SentinelAlertPayloadSchema>;
127
+ /**
128
+ * Event-type literal and version. Use the constants rather than the bare
129
+ * string so a future rename is grep-safe — same convention as
130
+ * `REFLECTION_COMPLETED_EVENT_TYPE`.
131
+ */
132
+ export declare const SENTINEL_ALERTED_EVENT_TYPE: "sentinel.alerted";
133
+ export declare const SENTINEL_ALERTED_SCHEMA_VERSION: "1";
134
+ //# sourceMappingURL=sentinel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../src/schemas/sentinel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;EAGhC,CAAA;AACF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,4CAA0C,CAAA;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAErE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;IAkBrC;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEH,CAAA;AACF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAA;AAE7E;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,oBAA8B,CAAA;AACtE,eAAO,MAAM,+BAA+B,KAAe,CAAA"}
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ import { TimestampSchema } from "./common.js";
3
+ /**
4
+ * Sentinel alerts — the wire format a Sentinel emits when it pattern-matches
5
+ * a suspicious shape in the event stream.
6
+ *
7
+ * Design lock: `docs/architecture/sentinels.md` (and Q7 of
8
+ * `docs/architecture/reflection-pass.md`, which settled the execution model).
9
+ * The short version:
10
+ *
11
+ * - Sentinels are an async tail of the event stream. They never block the
12
+ * Action Kernel; they emit `sentinel.alerted@1` events and a future
13
+ * (additive) `arbitrate` hook honours them on the *next* action that
14
+ * depends on a flagged subject.
15
+ * - This is a governance event, not an Observation. Like
16
+ * `reflection.completed@1`, the payload is the event payload directly and
17
+ * is NOT registered in the observation schema registry.
18
+ *
19
+ * Core owns the wire format only. The base class, the runner, and the
20
+ * concrete sentinels live in `@qmilab/lodestar-harness`.
21
+ */
22
+ /**
23
+ * What a sentinel alert is *about*. Kept deliberately small — the four
24
+ * epistemic-chain nouns a v0 sentinel can point at.
25
+ *
26
+ * `belief` is the load-bearing kind for the eventual kernel hook: the
27
+ * `arbitrate` lookup scopes recent alerts to a candidate action's
28
+ * `belief_dependencies`, so a sentinel that names a belief gates the next
29
+ * action that leans on it. `tool_sequence` is a synthetic subject — its
30
+ * `id` identifies the *completion* of a matched sequence (the id of the
31
+ * final action in the run), since no single chain-noun owns the pattern.
32
+ */
33
+ export const SentinelSubjectSchema = z.object({
34
+ kind: z.enum(["belief", "action", "decision", "tool_sequence"]),
35
+ id: z.string().min(1),
36
+ });
37
+ /**
38
+ * Triage weight. `critical` is reserved for patterns that, left unflagged,
39
+ * map onto a concrete attack (e.g. the read → external-egress → write
40
+ * exfiltration shape). `warning` is the default for "under-supported but
41
+ * not obviously hostile". `info` is for advisory signal a calibrator may
42
+ * later promote or demote.
43
+ */
44
+ export const SentinelSeveritySchema = z.enum(["info", "warning", "critical"]);
45
+ /**
46
+ * The payload of a `sentinel.alerted@1` event.
47
+ *
48
+ * `observed_event_ids` lists exactly the events the sentinel read to reach
49
+ * this conclusion; they are also the alert envelope's `causal_parent_ids`,
50
+ * so `lodestar report` can walk back from an alert to the events that
51
+ * triggered it.
52
+ *
53
+ * `detail` is rule-specific structured context (offending belief ids, the
54
+ * matched tool run, the confidence that tripped the floor, …). It is a
55
+ * record rather than a discriminated union so a new sentinel can ship
56
+ * without a core schema bump; the human-readable `message` is always
57
+ * present so an alert is legible without decoding `detail`.
58
+ *
59
+ * Every field is required (no optionals besides `rationale_id`) so the
60
+ * event-log writer's canonical hash and `JSON.stringify` never disagree on
61
+ * a dropped `undefined` key — the same discipline the firewall audit
62
+ * events follow.
63
+ */
64
+ export const SentinelAlertPayloadSchema = z.object({
65
+ alert_id: z.string().min(1),
66
+ sentinel_name: z.string().min(1).describe("Stable name of the emitting sentinel."),
67
+ rule: z
68
+ .string()
69
+ .min(1)
70
+ .describe("Stable id of the specific rule/pattern that fired within the sentinel."),
71
+ severity: SentinelSeveritySchema,
72
+ subject: SentinelSubjectSchema,
73
+ message: z.string().min(1).describe("Human-readable account of what tripped the sentinel."),
74
+ observed_event_ids: z
75
+ .array(z.string())
76
+ .min(1)
77
+ .describe("The events the sentinel read to reach this alert; also the alert's causal parents."),
78
+ detail: z
79
+ .record(z.string(), z.unknown())
80
+ .describe("Rule-specific structured context. May be empty; never undefined."),
81
+ detected_at: TimestampSchema,
82
+ /**
83
+ * Reserved. A sentinel may attach a generated Explanation id here, the
84
+ * way reflection proposals carry one. v0 sentinels rely on `message` +
85
+ * `detail` and leave this unset (omitted entirely, not `undefined`, so
86
+ * the canonical hash stays stable).
87
+ */
88
+ rationale_id: z.string().optional(),
89
+ });
90
+ /**
91
+ * Event-type literal and version. Use the constants rather than the bare
92
+ * string so a future rename is grep-safe — same convention as
93
+ * `REFLECTION_COMPLETED_EVENT_TYPE`.
94
+ */
95
+ export const SENTINEL_ALERTED_EVENT_TYPE = "sentinel.alerted";
96
+ export const SENTINEL_ALERTED_SCHEMA_VERSION = "1";
97
+ //# sourceMappingURL=sentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel.js","sourceRoot":"","sources":["../../src/schemas/sentinel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;IAC/D,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtB,CAAC,CAAA;AAGF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAA;AAG7E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,uCAAuC,CAAC;IAClF,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,wEAAwE,CAAC;IACrF,QAAQ,EAAE,sBAAsB;IAChC,OAAO,EAAE,qBAAqB;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sDAAsD,CAAC;IAC3F,kBAAkB,EAAE,CAAC;SAClB,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,oFAAoF,CAAC;IACjG,MAAM,EAAE,CAAC;SACN,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;SAC/B,QAAQ,CAAC,kEAAkE,CAAC;IAC/E,WAAW,EAAE,eAAe;IAC5B;;;;;OAKG;IACH,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAA;AAGF;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,kBAA2B,CAAA;AACtE,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qmilab/lodestar-core",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Schemas and types for the Lodestar epistemic chain. Part of Lodestar, the trust layer for AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "QMI Lab <hello@qmilab.com>",
@@ -34,12 +34,7 @@
34
34
  "default": "./dist/index.js"
35
35
  }
36
36
  },
37
- "files": [
38
- "dist",
39
- "src",
40
- "README.md",
41
- "LICENSE"
42
- ],
37
+ "files": ["dist", "src", "README.md", "LICENSE"],
43
38
  "publishConfig": {
44
39
  "access": "public",
45
40
  "provenance": true
package/src/index.ts CHANGED
@@ -28,5 +28,23 @@ export * from "./schemas/revision.js"
28
28
  // Event log envelope
29
29
  export * from "./schemas/event.js"
30
30
 
31
+ // Reflection (Batch 4) — proposals and the reflection.completed@1 payload
32
+ export * from "./schemas/reflection.js"
33
+
34
+ // Calibration — the report wire format + the calibration.computed@1 payload
35
+ export * from "./schemas/calibration.js"
36
+
37
+ // Probe pack format (Batch 4) — the lodestar.probe-pack.json manifest contract
38
+ export * from "./schemas/probe-pack.js"
39
+
40
+ // Sentinels (Batch 4) — the sentinel.alerted@1 alert wire format
41
+ export * from "./schemas/sentinel.js"
42
+
43
+ // Action policy (Policy Kernel) — the Policy / PolicyRule document wire format
44
+ export * from "./schemas/policy.js"
45
+
46
+ // Approval workflow (Policy Kernel) — ApprovalRequest + approval.* event payloads
47
+ export * from "./schemas/approval.js"
48
+
31
49
  // Schema registry
32
50
  export * as registry from "./registry.js"
@@ -60,10 +60,27 @@ export type ActionContract = z.infer<typeof ActionContractSchema>
60
60
 
61
61
  /**
62
62
  * Phases an action passes through.
63
+ *
64
+ * `pending_approval` is the parked state: arbitration returned a `hold`
65
+ * (the Policy Kernel's three-valued verdict — see
66
+ * `docs/architecture/policy-kernel.md`), so the action is neither approved
67
+ * nor rejected. An `ApprovalRequest` is opened and the world stays
68
+ * untouched — the two-phase discipline forbids `execute()` from
69
+ * `pending_approval` exactly as it forbids it from `proposed`. Only an
70
+ * Action-Kernel `resolve()` un-parks it: `approval.granted` → `approved`
71
+ * (which then runs the normal `execute()` gate, so TOCTOU revalidation
72
+ * still fires), `approval.denied` / `approval.expired` → `rejected`.
73
+ *
74
+ * Distinct from `halted`, which is a *terminal* mid-execution stop
75
+ * (`executing → halted`); `pending_approval` is a *pre-execution* wait.
76
+ *
77
+ * Additive (ratified 2026-06-03, `policy-kernel.md`): existing logs
78
+ * without this value still parse; readers gain one case.
63
79
  */
64
80
  export const ActionPhaseSchema = z.enum([
65
81
  "proposed",
66
82
  "arbitrating",
83
+ "pending_approval",
67
84
  "approved",
68
85
  "rejected",
69
86
  "executing",
@@ -100,7 +117,9 @@ export type AuditEvent = z.infer<typeof AuditEventSchema>
100
117
  *
101
118
  * Actions are the seventh link in the epistemic chain.
102
119
  * The phase field tracks the action through propose → arbitrate
103
- * → approved/rejected → executing → completed/failed/halted.
120
+ * → approved/rejected/pending_approval → executing
121
+ * → completed/failed/halted. A `pending_approval` action awaits an
122
+ * `ApprovalRequest` resolution before it can reach `approved`.
104
123
  *
105
124
  * Every Action carries an ActionContract. The Policy Kernel evaluates
106
125
  * the contract against current trust assignments and approval requirements
@@ -0,0 +1,136 @@
1
+ import { z } from "zod"
2
+ import { SignatureSchema } from "./actor.js"
3
+ import { TimestampSchema } from "./common.js"
4
+ import { RequiredAuthoritySchema } from "./policy.js"
5
+
6
+ /**
7
+ * The approval workflow wire formats — the first-class record of an action
8
+ * parked at `pending_approval` and the events that resolve it.
9
+ *
10
+ * Design lock: `docs/architecture/policy-kernel.md`, "The approval workflow".
11
+ * The discipline mirrors the sentinel / reflection governance events:
12
+ *
13
+ * - These are governance events, NOT Observations. Like `sentinel.alerted@1`,
14
+ * each payload is the event payload directly and is NOT registered in the
15
+ * observation schema registry.
16
+ * - Grant and deny are *distinct event types*, not one event with an
17
+ * `approved` flag. The type *is* the verdict, so a redundant boolean (which
18
+ * could disagree with the type on re-read) is omitted. When the resolution
19
+ * folds back into the action via the Action Kernel's `resolve()`, it lands
20
+ * in the action's existing `approval` field (`ApprovalEvent`), where a
21
+ * single boolean is the natural shape — so the stream view
22
+ * (type-discriminated) and the single-action view agree without duplicating
23
+ * the verdict on the wire.
24
+ * - No optional field is ever set to `undefined` — it is omitted entirely when
25
+ * unset (`deadline` and `reason` in particular), so the event-log writer's
26
+ * `canonicalHash` (undefined → null) and `JSON.stringify` (drops the key)
27
+ * cannot disagree on re-read.
28
+ *
29
+ * Core owns the wire format only. The lifecycle manager — opening a request on
30
+ * a hold, matching a resolution against `required_authority`, driving the
31
+ * Action-Kernel `resolve()` transition — lives in
32
+ * `@qmilab/lodestar-policy-kernel`.
33
+ */
34
+
35
+ /**
36
+ * The payload of an `approval.requested@1` event: a parked action awaiting a
37
+ * human (or auto-rule) verdict. `reason` is the matched rule's reason,
38
+ * verbatim. `required_authority` says what an approver must be (checked
39
+ * against the resolver's `Actor`); an empty object means any configured
40
+ * resolver may approve.
41
+ *
42
+ * `deadline` is the proxy's hold timeout (the MCP path cannot hold a
43
+ * `tools/call` open indefinitely without tripping client timeouts); it is
44
+ * *omitted entirely* in the in-process `guard.wrap()` path, where a hold can
45
+ * simply await the resolver — never set to `undefined`.
46
+ */
47
+ export const ApprovalRequestSchema = z.object({
48
+ request_id: z.string().min(1),
49
+ action_id: z.string().min(1).describe("the parked action, at phase pending_approval"),
50
+ reason: z.string().min(1).describe("the matched rule's reason, verbatim"),
51
+ required_authority: RequiredAuthoritySchema.describe(
52
+ "what an approver must be; checked against the resolver's Actor. Empty object = any configured resolver",
53
+ ),
54
+ requested_at: TimestampSchema,
55
+ deadline: TimestampSchema.optional().describe(
56
+ "ISO 8601 hold timeout (proxy path); omitted entirely in-process, never undefined",
57
+ ),
58
+ })
59
+ export type ApprovalRequest = z.infer<typeof ApprovalRequestSchema>
60
+
61
+ /**
62
+ * The payload of an `approval.granted@1` event. The event *type* is the
63
+ * verdict — there is no `approved` boolean. `reason` (the approver's note) is
64
+ * omitted entirely when unset.
65
+ *
66
+ * `signature` is an optional Ed25519 signature over the canonical resolution
67
+ * document (`{ request_id, action_id, kind, approver_id, reason?, at }`),
68
+ * produced by the approver's private key. When present it makes the granted
69
+ * event **self-verifying in the log**: a reader can later re-check the grant
70
+ * came from an operator-pinned approver key, not merely trust that the proxy
71
+ * verified it at promotion time. Its `signer_id` equals `approver_id` (the same
72
+ * actor that resolved). Omitted entirely when unset (never `undefined`), so the
73
+ * canonical-hash discipline above carries through; the cross-process proxy path
74
+ * requires it (a forged side-channel grant cannot un-park an action), while the
75
+ * in-process resolver path may omit it (same trusted process, no forgery
76
+ * surface). Hash + verification live in `@qmilab/lodestar-policy-kernel`.
77
+ */
78
+ export const ApprovalGrantedPayloadSchema = z.object({
79
+ request_id: z.string().min(1),
80
+ action_id: z.string().min(1),
81
+ approver_id: z.string().min(1).describe("actor_id of the resolver"),
82
+ reason: z.string().min(1).optional().describe("approver's note; omitted entirely when unset"),
83
+ at: TimestampSchema,
84
+ signature: SignatureSchema.optional().describe(
85
+ "Ed25519 signature over the canonical resolution; signer_id === approver_id; omitted entirely when unset",
86
+ ),
87
+ })
88
+ export type ApprovalGrantedPayload = z.infer<typeof ApprovalGrantedPayloadSchema>
89
+
90
+ /**
91
+ * The payload of an `approval.denied@1` event. Identical shape to
92
+ * `approval.granted@1` — the verdict is carried by the event type, not a
93
+ * field. Defined as its own schema (rather than re-exporting one shared
94
+ * object) so the two event types stay independently evolvable. `signature`
95
+ * follows the same contract as the grant payload (a denial is also authority-
96
+ * bearing — it must not be forgeable into un-holding via a later grant either).
97
+ */
98
+ export const ApprovalDeniedPayloadSchema = z.object({
99
+ request_id: z.string().min(1),
100
+ action_id: z.string().min(1),
101
+ approver_id: z.string().min(1).describe("actor_id of the resolver"),
102
+ reason: z.string().min(1).optional().describe("approver's note; omitted entirely when unset"),
103
+ at: TimestampSchema,
104
+ signature: SignatureSchema.optional().describe(
105
+ "Ed25519 signature over the canonical resolution; signer_id === approver_id; omitted entirely when unset",
106
+ ),
107
+ })
108
+ export type ApprovalDeniedPayload = z.infer<typeof ApprovalDeniedPayloadSchema>
109
+
110
+ /**
111
+ * The payload of an `approval.expired@1` event: the deadline passed with no
112
+ * human resolution. Carries no `approver_id` — no actor resolved it; the
113
+ * passage of the deadline did. The Action Kernel transitions the parked action
114
+ * to `rejected` on receipt (a timed-out hold is a soft denial the agent
115
+ * re-proposes; durable resume is deferred — `policy-kernel.md`).
116
+ */
117
+ export const ApprovalExpiredPayloadSchema = z.object({
118
+ request_id: z.string().min(1),
119
+ action_id: z.string().min(1),
120
+ at: TimestampSchema,
121
+ })
122
+ export type ApprovalExpiredPayload = z.infer<typeof ApprovalExpiredPayloadSchema>
123
+
124
+ /**
125
+ * Event-type literals and versions. Use the constants rather than the bare
126
+ * strings so a future rename is grep-safe — same convention as
127
+ * `SENTINEL_ALERTED_EVENT_TYPE` and `REFLECTION_COMPLETED_EVENT_TYPE`.
128
+ */
129
+ export const APPROVAL_REQUESTED_EVENT_TYPE = "approval.requested" as const
130
+ export const APPROVAL_REQUESTED_SCHEMA_VERSION = "1" as const
131
+ export const APPROVAL_GRANTED_EVENT_TYPE = "approval.granted" as const
132
+ export const APPROVAL_GRANTED_SCHEMA_VERSION = "1" as const
133
+ export const APPROVAL_DENIED_EVENT_TYPE = "approval.denied" as const
134
+ export const APPROVAL_DENIED_SCHEMA_VERSION = "1" as const
135
+ export const APPROVAL_EXPIRED_EVENT_TYPE = "approval.expired" as const
136
+ export const APPROVAL_EXPIRED_SCHEMA_VERSION = "1" as const
@@ -19,7 +19,13 @@ import {
19
19
  export const TruthStatusSchema = z.enum(["unverified", "supported", "contradicted", "superseded"])
20
20
  export type TruthStatus = z.infer<typeof TruthStatusSchema>
21
21
 
22
- export const RetrievalStatusSchema = z.enum(["hidden", "restricted", "normal", "privileged_only", "blocked"])
22
+ export const RetrievalStatusSchema = z.enum([
23
+ "hidden",
24
+ "restricted",
25
+ "normal",
26
+ "privileged_only",
27
+ "blocked",
28
+ ])
23
29
  export type RetrievalStatus = z.infer<typeof RetrievalStatusSchema>
24
30
 
25
31
  export const SecurityStatusSchema = z.enum(["clean", "suspicious", "quarantined", "malicious"])