@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.
- package/dist/app-runtime/index.d.ts +3 -0
- package/dist/app-runtime/index.d.ts.map +1 -1
- package/dist/app-runtime/index.js +3 -0
- package/dist/app-runtime/index.js.map +1 -1
- package/dist/app-runtime/lib/app-channel.d.ts +7 -0
- package/dist/app-runtime/lib/app-channel.d.ts.map +1 -0
- package/dist/app-runtime/lib/app-channel.js +11 -0
- package/dist/app-runtime/lib/app-channel.js.map +1 -0
- package/dist/app-runtime/lib/app-release.d.ts +24 -0
- package/dist/app-runtime/lib/app-release.d.ts.map +1 -0
- package/dist/app-runtime/lib/app-release.js +29 -0
- package/dist/app-runtime/lib/app-release.js.map +1 -0
- package/dist/app-runtime/lib/app.d.ts +3 -0
- package/dist/app-runtime/lib/app.d.ts.map +1 -1
- package/dist/app-runtime/lib/app.js +19 -4
- package/dist/app-runtime/lib/app.js.map +1 -1
- package/dist/app-runtime/lib/branding-config.d.ts.map +1 -1
- package/dist/app-runtime/lib/branding-config.js.map +1 -1
- package/dist/app-runtime/lib/delegated-session.js +1 -1
- package/dist/app-runtime/lib/delegated-session.js.map +1 -1
- package/dist/app-runtime/lib/surface-kind.d.ts +8 -0
- package/dist/app-runtime/lib/surface-kind.d.ts.map +1 -0
- package/dist/app-runtime/lib/surface-kind.js +12 -0
- package/dist/app-runtime/lib/surface-kind.js.map +1 -0
- package/dist/capability/index.d.ts +1 -0
- package/dist/capability/index.d.ts.map +1 -1
- package/dist/capability/index.js +1 -0
- package/dist/capability/index.js.map +1 -1
- package/dist/capability/lib/first-party-capability-domains.d.ts +29 -0
- package/dist/capability/lib/first-party-capability-domains.d.ts.map +1 -0
- package/dist/capability/lib/first-party-capability-domains.js +35 -0
- package/dist/capability/lib/first-party-capability-domains.js.map +1 -0
- package/dist/principal/index.d.ts +6 -0
- package/dist/principal/index.d.ts.map +1 -0
- package/dist/principal/index.js +22 -0
- package/dist/principal/index.js.map +1 -0
- package/dist/principal/lib/app-invocation-identity.d.ts +22 -0
- package/dist/principal/lib/app-invocation-identity.d.ts.map +1 -0
- package/dist/principal/lib/app-invocation-identity.js +36 -0
- package/dist/principal/lib/app-invocation-identity.js.map +1 -0
- package/dist/principal/lib/clamp-authority.d.ts +4 -0
- package/dist/principal/lib/clamp-authority.d.ts.map +1 -0
- package/dist/principal/lib/clamp-authority.js +23 -0
- package/dist/principal/lib/clamp-authority.js.map +1 -0
- package/dist/principal/lib/delegation-chain.d.ts +26 -0
- package/dist/principal/lib/delegation-chain.d.ts.map +1 -0
- package/dist/principal/lib/delegation-chain.js +81 -0
- package/dist/principal/lib/delegation-chain.js.map +1 -0
- package/dist/principal/lib/delegation-grant.d.ts +10 -0
- package/dist/principal/lib/delegation-grant.d.ts.map +1 -0
- package/dist/principal/lib/delegation-grant.js +12 -0
- package/dist/principal/lib/delegation-grant.js.map +1 -0
- package/dist/principal/lib/execution-principal.d.ts +20 -0
- package/dist/principal/lib/execution-principal.d.ts.map +1 -0
- package/dist/principal/lib/execution-principal.js +22 -0
- package/dist/principal/lib/execution-principal.js.map +1 -0
- package/package.json +1 -1
- package/src/app-runtime/index.ts +3 -0
- package/src/app-runtime/lib/app-channel.ts +32 -0
- package/src/app-runtime/lib/app-release.ts +90 -0
- package/src/app-runtime/lib/app.ts +48 -17
- package/src/app-runtime/lib/branding-config.ts +12 -6
- package/src/app-runtime/lib/delegated-session.ts +2 -2
- package/src/app-runtime/lib/surface-kind.ts +30 -0
- package/src/capability/index.ts +1 -0
- package/src/capability/lib/first-party-capability-domains.ts +76 -0
- package/src/principal/index.ts +5 -0
- package/src/principal/lib/app-invocation-identity.ts +95 -0
- package/src/principal/lib/clamp-authority.ts +64 -0
- package/src/principal/lib/delegation-chain.ts +145 -0
- package/src/principal/lib/delegation-grant.ts +36 -0
- 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>;
|