@xemahq/kernel-contracts 0.33.0 → 0.34.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 (72) hide show
  1. package/dist/app-runtime/index.d.ts +3 -0
  2. package/dist/app-runtime/index.d.ts.map +1 -1
  3. package/dist/app-runtime/index.js +3 -0
  4. package/dist/app-runtime/index.js.map +1 -1
  5. package/dist/app-runtime/lib/app-channel.d.ts +7 -0
  6. package/dist/app-runtime/lib/app-channel.d.ts.map +1 -0
  7. package/dist/app-runtime/lib/app-channel.js +11 -0
  8. package/dist/app-runtime/lib/app-channel.js.map +1 -0
  9. package/dist/app-runtime/lib/app-release.d.ts +24 -0
  10. package/dist/app-runtime/lib/app-release.d.ts.map +1 -0
  11. package/dist/app-runtime/lib/app-release.js +29 -0
  12. package/dist/app-runtime/lib/app-release.js.map +1 -0
  13. package/dist/app-runtime/lib/app.d.ts +3 -0
  14. package/dist/app-runtime/lib/app.d.ts.map +1 -1
  15. package/dist/app-runtime/lib/app.js +19 -4
  16. package/dist/app-runtime/lib/app.js.map +1 -1
  17. package/dist/app-runtime/lib/branding-config.d.ts.map +1 -1
  18. package/dist/app-runtime/lib/branding-config.js.map +1 -1
  19. package/dist/app-runtime/lib/delegated-session.js +1 -1
  20. package/dist/app-runtime/lib/delegated-session.js.map +1 -1
  21. package/dist/app-runtime/lib/surface-kind.d.ts +8 -0
  22. package/dist/app-runtime/lib/surface-kind.d.ts.map +1 -0
  23. package/dist/app-runtime/lib/surface-kind.js +12 -0
  24. package/dist/app-runtime/lib/surface-kind.js.map +1 -0
  25. package/dist/capability/index.d.ts +1 -0
  26. package/dist/capability/index.d.ts.map +1 -1
  27. package/dist/capability/index.js +1 -0
  28. package/dist/capability/index.js.map +1 -1
  29. package/dist/capability/lib/first-party-capability-domains.d.ts +29 -0
  30. package/dist/capability/lib/first-party-capability-domains.d.ts.map +1 -0
  31. package/dist/capability/lib/first-party-capability-domains.js +35 -0
  32. package/dist/capability/lib/first-party-capability-domains.js.map +1 -0
  33. package/dist/principal/index.d.ts +6 -0
  34. package/dist/principal/index.d.ts.map +1 -0
  35. package/dist/principal/index.js +22 -0
  36. package/dist/principal/index.js.map +1 -0
  37. package/dist/principal/lib/app-invocation-identity.d.ts +22 -0
  38. package/dist/principal/lib/app-invocation-identity.d.ts.map +1 -0
  39. package/dist/principal/lib/app-invocation-identity.js +36 -0
  40. package/dist/principal/lib/app-invocation-identity.js.map +1 -0
  41. package/dist/principal/lib/clamp-authority.d.ts +4 -0
  42. package/dist/principal/lib/clamp-authority.d.ts.map +1 -0
  43. package/dist/principal/lib/clamp-authority.js +23 -0
  44. package/dist/principal/lib/clamp-authority.js.map +1 -0
  45. package/dist/principal/lib/delegation-chain.d.ts +26 -0
  46. package/dist/principal/lib/delegation-chain.d.ts.map +1 -0
  47. package/dist/principal/lib/delegation-chain.js +81 -0
  48. package/dist/principal/lib/delegation-chain.js.map +1 -0
  49. package/dist/principal/lib/delegation-grant.d.ts +10 -0
  50. package/dist/principal/lib/delegation-grant.d.ts.map +1 -0
  51. package/dist/principal/lib/delegation-grant.js +12 -0
  52. package/dist/principal/lib/delegation-grant.js.map +1 -0
  53. package/dist/principal/lib/execution-principal.d.ts +20 -0
  54. package/dist/principal/lib/execution-principal.d.ts.map +1 -0
  55. package/dist/principal/lib/execution-principal.js +22 -0
  56. package/dist/principal/lib/execution-principal.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/app-runtime/index.ts +3 -0
  59. package/src/app-runtime/lib/app-channel.ts +32 -0
  60. package/src/app-runtime/lib/app-release.ts +90 -0
  61. package/src/app-runtime/lib/app.ts +48 -17
  62. package/src/app-runtime/lib/branding-config.ts +12 -6
  63. package/src/app-runtime/lib/delegated-session.ts +2 -2
  64. package/src/app-runtime/lib/surface-kind.ts +30 -0
  65. package/src/capability/index.ts +1 -0
  66. package/src/capability/lib/first-party-capability-domains.ts +76 -0
  67. package/src/principal/index.ts +5 -0
  68. package/src/principal/lib/app-invocation-identity.ts +95 -0
  69. package/src/principal/lib/clamp-authority.ts +64 -0
  70. package/src/principal/lib/delegation-chain.ts +145 -0
  71. package/src/principal/lib/delegation-grant.ts +36 -0
  72. package/src/principal/lib/execution-principal.ts +74 -0
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ SubjectKind,
4
+ SubjectRefSchema,
5
+ type SubjectRef,
6
+ } from '../../subject';
7
+
8
+ /**
9
+ * The two ways an app invocation names its `actor`.
10
+ *
11
+ * `AppInvocationIdentity` is SUGAR over the `ExecutionPrincipal.actor` field —
12
+ * it resolves to exactly one `SubjectRef` (via `resolveInvocationActor`). It
13
+ * adds no authority of its own; it only says WHOSE authority the app is
14
+ * exercising.
15
+ *
16
+ * **`AsApp` is not a bypass.** A scheduled app runs as `executor = app:<id>`
17
+ * WITH `actor = app:<id>` — the app IS the actor, acting purely as itself. It
18
+ * must hold an explicit app role IN ITS OWN app to do anything; there is no
19
+ * ambient "the scheduler said so" authority. A scheduled job does NOT skip row
20
+ * policy and does NOT skip field policy. `AsApp` narrows the app to its own
21
+ * granted authority exactly like any other principal — running unattended buys
22
+ * it nothing that running on behalf of a user would not have to earn through the
23
+ * same `clampAuthority` intersection.
24
+ */
25
+ export enum AppInvocationMode {
26
+ /** The app acts on behalf of a signed-in user; `actor` is that user. */
27
+ OnBehalfOfUser = 'on-behalf-of-user',
28
+ /** The app acts as itself (e.g. a schedule); `actor === executor === app:<id>`. */
29
+ AsApp = 'as-app',
30
+ }
31
+
32
+ export const AppInvocationModeSchema = z.nativeEnum(AppInvocationMode);
33
+
34
+ /**
35
+ * `OnBehalfOfUser` — the app is exercising a specific user's authority. `user`
36
+ * becomes `ExecutionPrincipal.actor`; the app is the `executor`.
37
+ */
38
+ export interface OnBehalfOfUserIdentity {
39
+ mode: AppInvocationMode.OnBehalfOfUser;
40
+ /** The signed-in subject whose authority the app exercises. */
41
+ user: SubjectRef;
42
+ /** The app doing the acting (becomes `executor`). */
43
+ appId: string;
44
+ }
45
+
46
+ /**
47
+ * `AsApp` — the app acts as itself. `actor` and `executor` are BOTH
48
+ * `app:<appId>`; see the `AppInvocationMode` docblock — this is not a bypass.
49
+ */
50
+ export interface AsAppIdentity {
51
+ mode: AppInvocationMode.AsApp;
52
+ /** The app acting as its own actor and executor. */
53
+ appId: string;
54
+ }
55
+
56
+ export type AppInvocationIdentity = OnBehalfOfUserIdentity | AsAppIdentity;
57
+
58
+ const onBehalfOfUserShape = z.object({
59
+ mode: z.literal(AppInvocationMode.OnBehalfOfUser),
60
+ user: SubjectRefSchema,
61
+ appId: z.string().min(1),
62
+ });
63
+
64
+ const asAppShape = z.object({
65
+ mode: z.literal(AppInvocationMode.AsApp),
66
+ appId: z.string().min(1),
67
+ });
68
+
69
+ export const OnBehalfOfUserIdentitySchema =
70
+ onBehalfOfUserShape as z.ZodType<OnBehalfOfUserIdentity>;
71
+
72
+ export const AsAppIdentitySchema = asAppShape as z.ZodType<AsAppIdentity>;
73
+
74
+ export const AppInvocationIdentitySchema = z.discriminatedUnion('mode', [
75
+ onBehalfOfUserShape,
76
+ asAppShape,
77
+ ]) as z.ZodType<AppInvocationIdentity>;
78
+
79
+ /**
80
+ * Resolve an `AppInvocationIdentity` to the `ExecutionPrincipal.actor` it is
81
+ * sugar for. `OnBehalfOfUser` → the named user. `AsApp` → the app itself
82
+ * (`app:<appId>`), which is ALSO the executor — an app acting as itself has no
83
+ * elevated authority (see the `AppInvocationMode` docblock). Total over the
84
+ * closed union; a new mode is a compile error here, never a silent default.
85
+ */
86
+ export function resolveInvocationActor(
87
+ identity: AppInvocationIdentity,
88
+ ): SubjectRef {
89
+ switch (identity.mode) {
90
+ case AppInvocationMode.OnBehalfOfUser:
91
+ return identity.user;
92
+ case AppInvocationMode.AsApp:
93
+ return { kind: SubjectKind.App, id: identity.appId };
94
+ }
95
+ }
@@ -0,0 +1,64 @@
1
+ import type { CapabilityRef } from '../../capability';
2
+
3
+ /**
4
+ * `clampAuthority` — the delegation intersection rule (plan D6).
5
+ *
6
+ * effective(P) = grants(P.executor) ∩ effective(P.actor)
7
+ *
8
+ * The effective authority of a principal is the INTERSECTION of what the
9
+ * executor is granted with what the actor already effectively holds. It is
10
+ * NEVER a union: a delegate can only ever NARROW authority, never widen it. An
11
+ * app can never do more than its caller.
12
+ *
13
+ * Structurally it cannot widen, and this is the load-bearing property: every
14
+ * element of the returned set is drawn from `executorGrants` AND must also be
15
+ * present in `actorEffective`, so the result is a subset of BOTH inputs
16
+ * (`clamp(A, B) ⊆ A` and `clamp(A, B) ⊆ B`). There is no code path that adds a
17
+ * ref present in only one operand.
18
+ *
19
+ * An EMPTY intersection is the default and correct outcome — not an error, not a
20
+ * signal to fall back to either operand. A principal whose executor shares no
21
+ * capability with its actor is authorized for nothing, and that denial is the
22
+ * whole point (it is THE test that must exist before the Wave 10 function
23
+ * sandbox: a delegate holding a capability its actor lacks is denied).
24
+ *
25
+ * Pure and side-effect free. Inputs are read-only sets of `CapabilityRef`; the
26
+ * result is a fresh set (the smaller operand is iterated for efficiency, but the
27
+ * membership rule is symmetric so the result is identical either way).
28
+ */
29
+ export function clampAuthority(
30
+ executorGrants: ReadonlySet<CapabilityRef>,
31
+ actorEffective: ReadonlySet<CapabilityRef>,
32
+ ): Set<CapabilityRef> {
33
+ const [smaller, larger] =
34
+ executorGrants.size <= actorEffective.size
35
+ ? [executorGrants, actorEffective]
36
+ : [actorEffective, executorGrants];
37
+
38
+ const intersection = new Set<CapabilityRef>();
39
+ for (const ref of smaller) {
40
+ if (larger.has(ref)) {
41
+ intersection.add(ref);
42
+ }
43
+ }
44
+ return intersection;
45
+ }
46
+
47
+ /**
48
+ * Fold `clampAuthority` down a whole chain of authority sets, in order.
49
+ * `clampAuthorityChain([grants(executor), effective(actor), …])` reduces every
50
+ * operand by intersection, so ANY empty operand collapses the whole result to
51
+ * empty. Every operand narrows; none widens. An empty input array is itself an
52
+ * empty authority set (nothing to be authorized for) — never a wildcard.
53
+ */
54
+ export function clampAuthorityChain(
55
+ authoritySets: ReadonlyArray<ReadonlySet<CapabilityRef>>,
56
+ ): Set<CapabilityRef> {
57
+ if (authoritySets.length === 0) {
58
+ return new Set<CapabilityRef>();
59
+ }
60
+ return authoritySets.reduce<Set<CapabilityRef>>(
61
+ (acc, next) => clampAuthority(acc, next),
62
+ new Set<CapabilityRef>(authoritySets[0]),
63
+ );
64
+ }
@@ -0,0 +1,145 @@
1
+ import { z } from 'zod';
2
+ import { subjectRefToString, type SubjectRef } from '../../subject';
3
+ import type { DelegationGrant } from './delegation-grant';
4
+
5
+ /**
6
+ * Maximum number of hops permitted in a single delegation chain. Delegation is
7
+ * bounded: user → app → workflow → agent is the deepest legitimate path, so a
8
+ * chain of more than THREE grants is rejected outright. This is a hard cap, not
9
+ * a truncation point — an over-long chain is an error, never silently clipped.
10
+ */
11
+ export const MAX_DELEGATION_DEPTH = 3;
12
+
13
+ /**
14
+ * Closed set of delegation-chain validation failures. Every rejection carries a
15
+ * structured code so callers branch without string parsing.
16
+ */
17
+ export enum DelegationErrorCode {
18
+ /** The chain has more hops than `MAX_DELEGATION_DEPTH`. */
19
+ ChainTooDeep = 'DELEGATION_CHAIN_TOO_DEEP',
20
+ /** A subject appears more than once across the chain — a delegation cycle. */
21
+ Cycle = 'DELEGATION_CHAIN_CYCLE',
22
+ /** Hop `i`'s `to` is not hop `i+1`'s `from` — the chain is not contiguous. */
23
+ Discontinuous = 'DELEGATION_CHAIN_DISCONTINUOUS',
24
+ /** The chain's first `from` is not the principal's `actor`. */
25
+ NotAnchoredAtActor = 'DELEGATION_CHAIN_NOT_ANCHORED_AT_ACTOR',
26
+ /** The chain's last `to` is not the principal's `executor`. */
27
+ NotAnchoredAtExecutor = 'DELEGATION_CHAIN_NOT_ANCHORED_AT_EXECUTOR',
28
+ /**
29
+ * The chain is empty but `actor` and `executor` differ. An empty chain is
30
+ * only valid for a self-acting principal (`actor === executor`, e.g. a
31
+ * scheduled `AsApp` app); claiming delegation with no hops is malformed.
32
+ */
33
+ EmptyChainMismatch = 'DELEGATION_CHAIN_EMPTY_MISMATCH',
34
+ }
35
+
36
+ export const DelegationErrorCodeSchema = z.nativeEnum(DelegationErrorCode);
37
+
38
+ /**
39
+ * Structured, fail-fast error thrown by `resolveDelegationChain`. Never a
40
+ * warning, never a coercion — an invalid chain aborts resolution.
41
+ */
42
+ export class DelegationChainError extends Error {
43
+ readonly code: DelegationErrorCode;
44
+
45
+ constructor(args: { code: DelegationErrorCode; message: string }) {
46
+ super(args.message);
47
+ this.name = 'DelegationChainError';
48
+ this.code = args.code;
49
+ }
50
+ }
51
+
52
+ function subjectsEqual(a: SubjectRef, b: SubjectRef): boolean {
53
+ return subjectRefToString(a) === subjectRefToString(b);
54
+ }
55
+
56
+ /**
57
+ * Validate a delegation chain against its principal's `actor`/`executor`
58
+ * endpoints and return the (frozen) chain when it is well-formed.
59
+ *
60
+ * Fail-fast — throws `DelegationChainError` on ANY of:
61
+ *
62
+ * - a chain longer than `MAX_DELEGATION_DEPTH` (`ChainTooDeep`);
63
+ * - a cycle: any subject appearing twice in the hop sequence (`Cycle`);
64
+ * - a chain whose first `from` is not `actor` (`NotAnchoredAtActor`);
65
+ * - a chain whose last `to` is not `executor` (`NotAnchoredAtExecutor`);
66
+ * - a non-contiguous chain where hop i's `to` ≠ hop i+1's `from`
67
+ * (`Discontinuous`);
68
+ * - an empty chain when `actor` ≠ `executor` (`EmptyChainMismatch`).
69
+ *
70
+ * Cycles and over-depth are ERRORS, never truncations: the chain is rejected
71
+ * whole, never clipped to fit.
72
+ */
73
+ export function resolveDelegationChain(args: {
74
+ actor: SubjectRef;
75
+ executor: SubjectRef;
76
+ chain: readonly DelegationGrant[];
77
+ }): readonly DelegationGrant[] {
78
+ const { actor, executor, chain } = args;
79
+
80
+ // Empty chain: only a self-acting principal (actor === executor) may omit
81
+ // delegation. Anything else is a delegation claim with no proof.
82
+ if (chain.length === 0) {
83
+ if (!subjectsEqual(actor, executor)) {
84
+ throw new DelegationChainError({
85
+ code: DelegationErrorCode.EmptyChainMismatch,
86
+ message: `Empty delegation chain requires actor === executor, but actor "${subjectRefToString(
87
+ actor,
88
+ )}" ≠ executor "${subjectRefToString(executor)}".`,
89
+ });
90
+ }
91
+ return Object.freeze([]);
92
+ }
93
+
94
+ if (chain.length > MAX_DELEGATION_DEPTH) {
95
+ throw new DelegationChainError({
96
+ code: DelegationErrorCode.ChainTooDeep,
97
+ message: `Delegation chain has ${chain.length} hops; the maximum is ${MAX_DELEGATION_DEPTH}.`,
98
+ });
99
+ }
100
+
101
+ // Anchor: first from === actor, last to === executor.
102
+ if (!subjectsEqual(chain[0]!.from, actor)) {
103
+ throw new DelegationChainError({
104
+ code: DelegationErrorCode.NotAnchoredAtActor,
105
+ message: `Delegation chain must start at actor "${subjectRefToString(
106
+ actor,
107
+ )}", but its first hop starts at "${subjectRefToString(chain[0]!.from)}".`,
108
+ });
109
+ }
110
+ const last = chain[chain.length - 1]!;
111
+ if (!subjectsEqual(last.to, executor)) {
112
+ throw new DelegationChainError({
113
+ code: DelegationErrorCode.NotAnchoredAtExecutor,
114
+ message: `Delegation chain must end at executor "${subjectRefToString(
115
+ executor,
116
+ )}", but its last hop ends at "${subjectRefToString(last.to)}".`,
117
+ });
118
+ }
119
+
120
+ // Contiguity + cycle detection over the node sequence
121
+ // [from0, to0, to1, …, toN-1].
122
+ const seen = new Set<string>();
123
+ const firstKey = subjectRefToString(chain[0]!.from);
124
+ seen.add(firstKey);
125
+ for (let i = 0; i < chain.length; i += 1) {
126
+ if (i > 0 && !subjectsEqual(chain[i - 1]!.to, chain[i]!.from)) {
127
+ throw new DelegationChainError({
128
+ code: DelegationErrorCode.Discontinuous,
129
+ message: `Delegation chain is not contiguous at hop ${i}: previous "to" "${subjectRefToString(
130
+ chain[i - 1]!.to,
131
+ )}" ≠ this "from" "${subjectRefToString(chain[i]!.from)}".`,
132
+ });
133
+ }
134
+ const toKey = subjectRefToString(chain[i]!.to);
135
+ if (seen.has(toKey)) {
136
+ throw new DelegationChainError({
137
+ code: DelegationErrorCode.Cycle,
138
+ message: `Delegation chain contains a cycle: subject "${toKey}" appears more than once.`,
139
+ });
140
+ }
141
+ seen.add(toKey);
142
+ }
143
+
144
+ return Object.freeze([...chain]);
145
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import { SubjectRefSchema, type SubjectRef } from '../../subject';
3
+
4
+ /**
5
+ * `DelegationGrant` — one hop in a delegation chain. It records HOW authority
6
+ * moved from one subject to the next, so the PDP and the audit trail can prove
7
+ * the path an invocation took (user → app → workflow → agent → connector)
8
+ * rather than collapsing it to "the workflow service called a connector".
9
+ *
10
+ * Deliberately minimal — `{ from, to, grantedAt, reason? }` — and it does NOT
11
+ * carry a per-hop capability subset. Justification: effective authority is
12
+ * resolved by `clampAuthority` as `grants(executor) ∩ effective(actor)` from the
13
+ * authorization plane's grant tables keyed by SUBJECT, not from data embedded in
14
+ * the chain. A grant here is a PROVENANCE record (who handed authority to whom,
15
+ * when, and why), never a capability carrier — putting a capability set on the
16
+ * hop would duplicate the clamp responsibility and create a second, forgeable
17
+ * source of truth for "what may this delegate do". `grantedAt` (ISO-8601) plus
18
+ * an optional human `reason` is the whole audit payload a hop needs.
19
+ */
20
+ export interface DelegationGrant {
21
+ /** The subject handing authority DOWN at this hop. */
22
+ from: SubjectRef;
23
+ /** The subject RECEIVING authority at this hop. */
24
+ to: SubjectRef;
25
+ /** ISO-8601 timestamp at which this hop was minted. */
26
+ grantedAt: string;
27
+ /** Optional human-readable rationale for the delegation (audit only). */
28
+ reason?: string;
29
+ }
30
+
31
+ export const DelegationGrantSchema = z.object({
32
+ from: SubjectRefSchema,
33
+ to: SubjectRefSchema,
34
+ grantedAt: z.string().datetime(),
35
+ reason: z.string().min(1).optional(),
36
+ }) as z.ZodType<DelegationGrant>;
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ SubjectRefSchema,
4
+ TokenClassSchema,
5
+ type SubjectRef,
6
+ type TokenClass,
7
+ } from '../../subject';
8
+ import {
9
+ ExecutionEnvironmentRefSchema,
10
+ type ExecutionEnvironmentRef,
11
+ } from '../../execution-environment';
12
+ import { AppChannelSchema, type AppChannel } from '../../app-runtime';
13
+ import { DelegationGrantSchema, type DelegationGrant } from './delegation-grant';
14
+
15
+ /**
16
+ * `ExecutionPrincipal` — the SINGLE delegation contract (plan D5).
17
+ *
18
+ * The tree previously answered "who is acting, on whose behalf, with what
19
+ * authority" in six divergent coats: `SubjectKind`, `TokenClass`,
20
+ * `SubjectRef.actingFor`, delegated sessions, external subjects, anonymous
21
+ * sessions, and a PDP "delegation clamp". `ExecutionPrincipal` is the one
22
+ * answer. It SURVIVES the whole hop path user → app → workflow → agent →
23
+ * connector — if it did not, the audit trail would collapse to "the workflow
24
+ * service called a connector" and the PDP could not prove HOW authority was
25
+ * delegated.
26
+ *
27
+ * - `actor` is the ORIGINAL human or external subject whose authority is being
28
+ * exercised. `executor` is what is actually running (app | workflow | agent |
29
+ * service). Effective authority is `clampAuthority(grants(executor),
30
+ * effective(actor))` — an intersection, never a union.
31
+ * - `delegationChain` records every hop; `resolveDelegationChain` bounds it to
32
+ * `MAX_DELEGATION_DEPTH` and rejects cycles fail-fast.
33
+ * - `channel` (preview/live), `appId`, `releaseId`, `tenantId`, `purpose` are
34
+ * the invocation-context fields the PDP and audit trail stamp; all optional
35
+ * because not every principal is an app invocation.
36
+ */
37
+ export interface ExecutionPrincipal {
38
+ /** The original human or external subject on whose behalf execution runs. */
39
+ actor: SubjectRef;
40
+ /** What is actually running: app | workflow | agent | service. */
41
+ executor: SubjectRef;
42
+ /** Ordered record of how authority moved from `actor` to `executor`. */
43
+ delegationChain: DelegationGrant[];
44
+ /** The minted token class carried by this invocation. */
45
+ tokenClass: TokenClass;
46
+ /** The permission-plane environment this invocation is tagged with. */
47
+ environment: ExecutionEnvironmentRef;
48
+ /** The single tenant org this invocation is fenced to. */
49
+ orgId: string;
50
+ /** The app being invoked, when this is an app invocation. */
51
+ appId?: string;
52
+ /** The pinned release of `appId`, when applicable. */
53
+ releaseId?: string;
54
+ /** Which definition + data the invocation sees (preview | live). */
55
+ channel?: AppChannel;
56
+ /** Sub-tenant scope within `orgId`, when the app is multi-tenant. */
57
+ tenantId?: string;
58
+ /** Free-form audit annotation of why this invocation is happening. */
59
+ purpose?: string;
60
+ }
61
+
62
+ export const ExecutionPrincipalSchema = z.object({
63
+ actor: SubjectRefSchema,
64
+ executor: SubjectRefSchema,
65
+ delegationChain: z.array(DelegationGrantSchema),
66
+ tokenClass: TokenClassSchema,
67
+ environment: ExecutionEnvironmentRefSchema,
68
+ orgId: z.string().min(1),
69
+ appId: z.string().min(1).optional(),
70
+ releaseId: z.string().min(1).optional(),
71
+ channel: AppChannelSchema.optional(),
72
+ tenantId: z.string().min(1).optional(),
73
+ purpose: z.string().min(1).optional(),
74
+ }) as z.ZodType<ExecutionPrincipal>;