@godspeedai/cognate-runtime-bun 0.0.0-stage → 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/LICENSE +201 -0
- package/README.md +39 -2
- package/package.json +38 -4
- package/src/cognition.ts +66 -0
- package/src/core.ts +36 -0
- package/src/executor.ts +423 -0
- package/src/fold.ts +96 -0
- package/src/governance-env.ts +35 -0
- package/src/governed-operation.ts +248 -0
- package/src/index.ts +95 -0
- package/src/inspect.ts +58 -0
- package/src/invoker.ts +145 -0
- package/src/remote-offers.ts +60 -0
- package/src/runtime.ts +235 -0
- package/src/sea-forge-authority.ts +401 -0
- package/src/service.ts +517 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// GovernedOperation: the single choke point for every consequential Cognate decision (migration Stage 5).
|
|
2
|
+
//
|
|
3
|
+
// Cognate operates; SEA-Forge governs. Two things are kept apart (authority != capability):
|
|
4
|
+
// - local policy (ActionPolicy / kernel Policy) says what this runtime is *able* to permit, and
|
|
5
|
+
// - the governance authority (SEA-Forge) says whether the operation is *authorized*.
|
|
6
|
+
// In `sea-forge` mode both must allow. In `off` mode only local policy applies, and every decision says so.
|
|
7
|
+
// Only `allow` proceeds. Every other disposition is refused here; later stages add constraint handling and
|
|
8
|
+
// escalation. Nothing outside this file may call `ActionPolicy.authorize` or the user's kernel `Policy`.
|
|
9
|
+
import type { Caller } from "@godspeedai/cognate-runtime-api";
|
|
10
|
+
import type { AuthorizationRequest, Policy, PolicyDecision } from "@godspeedai/cognate-kernel-api";
|
|
11
|
+
import type { ActionPolicy } from "./index.ts";
|
|
12
|
+
|
|
13
|
+
export type GovernanceMode = "sea-forge" | "off";
|
|
14
|
+
|
|
15
|
+
/** CEP authority dispositions. Never a boolean (CEP profile `authority_decision`). */
|
|
16
|
+
export type Disposition = "allow" | "deny" | "escalate" | "unknown" | "constrained_allow" | "boundary" | "degraded";
|
|
17
|
+
|
|
18
|
+
export type ActionName = Parameters<ActionPolicy["authorize"]>[0]["action"];
|
|
19
|
+
|
|
20
|
+
export type GovernedOperation =
|
|
21
|
+
| { readonly kind: "action"; readonly action: ActionName; readonly caller: Caller; readonly resource: string }
|
|
22
|
+
| { readonly kind: "capability"; readonly request: AuthorizationRequest };
|
|
23
|
+
|
|
24
|
+
/** One constraint the authority attached to a constrained disposition, e.g. `{ type: "timeout_secs", value: "30" }`. */
|
|
25
|
+
export interface GovernanceConstraint {
|
|
26
|
+
readonly type: string;
|
|
27
|
+
readonly value: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Installs one kind of constraint for one operation. It returns true only when the constraint is actually
|
|
32
|
+
* in force for that operation; any other outcome (false or a throw) means the constraint is not enforced.
|
|
33
|
+
*/
|
|
34
|
+
export interface ConstraintEnforcer {
|
|
35
|
+
apply(constraint: GovernanceConstraint, operation: GovernedOperation): boolean | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Which authority decision allowed an operation. It travels in the recorded decision metadata so everything
|
|
40
|
+
* downstream (the execution trace, the evidence, the settlement) can name the decision it hangs from.
|
|
41
|
+
*/
|
|
42
|
+
export interface AuthorityReference {
|
|
43
|
+
readonly operationId: string;
|
|
44
|
+
readonly requestEnvelopeId: string;
|
|
45
|
+
readonly decisionEnvelopeId: string;
|
|
46
|
+
readonly worldRef: string;
|
|
47
|
+
/** The authority's own record of the decision (e.g. a ledger entry), when it gave one. */
|
|
48
|
+
readonly decisionRef?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An escalation that a human can approve: the approval to wait for and when its window closes. */
|
|
52
|
+
export interface GovernanceEscalation {
|
|
53
|
+
readonly approvalId: string;
|
|
54
|
+
/** Epoch ms, when the authority said the approval window closes. */
|
|
55
|
+
readonly expiresAt?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Revalidation after an escalation was approved: the approval being relied on. */
|
|
59
|
+
export interface ApprovalReference {
|
|
60
|
+
readonly approvalId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ApprovalState = "pending" | "approved" | "rejected" | "expired" | "used" | "unknown";
|
|
64
|
+
|
|
65
|
+
export interface GovernanceDecision {
|
|
66
|
+
readonly disposition: Disposition;
|
|
67
|
+
readonly reason: string;
|
|
68
|
+
/** Required by CEP for boundary / constrained_allow / degraded. */
|
|
69
|
+
readonly policyBasis?: readonly string[];
|
|
70
|
+
readonly constraints?: readonly GovernanceConstraint[];
|
|
71
|
+
/** Required by CEP for degraded. Each must have an enforcer registered under `control:<name>`. */
|
|
72
|
+
readonly compensatingControls?: readonly string[];
|
|
73
|
+
/** Reference to the authority's decision record, when one exists. */
|
|
74
|
+
readonly decisionRef?: string;
|
|
75
|
+
/** Present on an `escalate` that a human approval can satisfy. */
|
|
76
|
+
readonly escalation?: GovernanceEscalation;
|
|
77
|
+
/** Which decision this was, when the authority identifies it. */
|
|
78
|
+
readonly authority?: AuthorityReference;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What SEA-Forge implements (Stage 6 supplies the CEP-backed implementation). */
|
|
82
|
+
export interface GovernanceAuthority {
|
|
83
|
+
/** `approval` is set only when revalidating an operation whose escalation has since been approved. */
|
|
84
|
+
decide(operation: GovernedOperation, worldRef: string, approval?: ApprovalReference): GovernanceDecision | Promise<GovernanceDecision>;
|
|
85
|
+
/** Where an escalation stands, if the authority can say. Used only to know when to try again; never to allow. */
|
|
86
|
+
approvalState?(approvalId: string): Promise<ApprovalState>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface GovernanceOptions {
|
|
90
|
+
readonly mode: GovernanceMode;
|
|
91
|
+
readonly authority?: GovernanceAuthority;
|
|
92
|
+
/** Immutable `world:<name>@sha256:<64 hex>` all operations of this runtime are governed against. */
|
|
93
|
+
readonly worldRef?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Enforcers by constraint type (`timeout_secs`) or `control:<name>` for compensating controls. None by
|
|
96
|
+
* default, so constrained dispositions are refused until a host registers the means to honor them.
|
|
97
|
+
*/
|
|
98
|
+
readonly enforcers?: Readonly<Record<string, ConstraintEnforcer>>;
|
|
99
|
+
/** When set (>0) and the authority can report approval state, poll for approved escalations this often. */
|
|
100
|
+
readonly approvalPollMs?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface GovernedResult {
|
|
104
|
+
readonly allow: boolean;
|
|
105
|
+
readonly mode: GovernanceMode;
|
|
106
|
+
readonly disposition: Disposition;
|
|
107
|
+
readonly reason: string;
|
|
108
|
+
readonly decisionRef?: string;
|
|
109
|
+
/** Constraints that were enforced before this operation was allowed (empty for plain allow). */
|
|
110
|
+
readonly applied?: readonly GovernanceConstraint[];
|
|
111
|
+
/** Present when the operation was refused because a human approval could satisfy it. */
|
|
112
|
+
readonly escalation?: GovernanceEscalation;
|
|
113
|
+
/** The decision that allowed (or refused) this, for downstream trace, evidence and settlement. */
|
|
114
|
+
readonly authority?: AuthorityReference;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const WORLD_REF = /^world:[a-z0-9]+(?:-[a-z0-9]+)*@sha256:[0-9a-f]{64}$/;
|
|
118
|
+
|
|
119
|
+
export function isWorldRef(text: unknown): text is string {
|
|
120
|
+
return typeof text === "string" && text.length <= 200 && WORLD_REF.test(text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Fails at construction, never at first use: `sea-forge` mode without authority and a world is a bug. */
|
|
124
|
+
export function validateGovernance(options: GovernanceOptions): void {
|
|
125
|
+
if (options.mode !== "sea-forge" && options.mode !== "off") throw new Error(`unsupported governance mode: ${String(options.mode)}`);
|
|
126
|
+
if (options.mode === "sea-forge") {
|
|
127
|
+
if (!options.authority) throw new Error("governance mode sea-forge requires an authority");
|
|
128
|
+
if (!isWorldRef(options.worldRef)) throw new Error("governance mode sea-forge requires a valid worldRef");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** What the invoker needs to observe about one capability authorization. */
|
|
133
|
+
export interface CapabilityHooks {
|
|
134
|
+
/** Set when revalidating an approved escalation. */
|
|
135
|
+
readonly approval?: ApprovalReference;
|
|
136
|
+
readonly applied?: (applied: readonly GovernanceConstraint[]) => void;
|
|
137
|
+
readonly escalated?: (escalation: GovernanceEscalation) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class Governor {
|
|
141
|
+
constructor(
|
|
142
|
+
private readonly local: ActionPolicy,
|
|
143
|
+
private readonly kernelPolicy: Policy,
|
|
144
|
+
private readonly options: GovernanceOptions,
|
|
145
|
+
) {
|
|
146
|
+
validateGovernance(options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get mode(): GovernanceMode {
|
|
150
|
+
return this.options.mode;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Decide one operation. Never throws for a refusal; throws only on programmer error. */
|
|
154
|
+
async govern(operation: GovernedOperation, approval?: ApprovalReference): Promise<GovernedResult> {
|
|
155
|
+
const mode = this.options.mode;
|
|
156
|
+
const localDecision = await this.localDecision(operation);
|
|
157
|
+
if (!localDecision.allow) return { allow: false, mode, disposition: "deny", reason: localDecision.reason };
|
|
158
|
+
if (mode === "off") return { allow: true, mode, disposition: "allow", reason: `${localDecision.reason} (governance off: local policy only)` };
|
|
159
|
+
|
|
160
|
+
let decision: GovernanceDecision;
|
|
161
|
+
try {
|
|
162
|
+
decision = await this.options.authority!.decide(operation, this.options.worldRef!, approval);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// An authority that cannot answer is not an authority that said yes.
|
|
165
|
+
return { allow: false, mode, disposition: "unknown", reason: `governance authority unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
166
|
+
}
|
|
167
|
+
return this.interpret(mode, decision, operation);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async localDecision(operation: GovernedOperation): Promise<{ allow: boolean; reason: string }> {
|
|
171
|
+
if (operation.kind === "action") return this.local.authorize({ caller: operation.caller, action: operation.action, resource: operation.resource });
|
|
172
|
+
return this.kernelPolicy.authorize(operation.request);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async interpret(mode: GovernanceMode, decision: GovernanceDecision, operation: GovernedOperation): Promise<GovernedResult> {
|
|
176
|
+
if (!decision || !KNOWN.has(decision.disposition)) return { allow: false, mode, disposition: "unknown", reason: "governance authority returned an unrecognized disposition" };
|
|
177
|
+
const ref = { ...(decision.decisionRef === undefined ? {} : { decisionRef: decision.decisionRef }), ...(decision.authority ? { authority: decision.authority } : {}) };
|
|
178
|
+
const refuse = (disposition: Disposition, reason: string): GovernedResult => ({ allow: false, mode, disposition, reason, ...ref });
|
|
179
|
+
if (decision.disposition === "allow") return { allow: true, mode, disposition: "allow", reason: decision.reason, ...ref };
|
|
180
|
+
if (decision.disposition === "escalate" && decision.escalation) {
|
|
181
|
+
return { ...refuse("escalate", `${decision.reason}; awaiting approval ${decision.escalation.approvalId}`), escalation: decision.escalation };
|
|
182
|
+
}
|
|
183
|
+
if (!CONSTRAINED.has(decision.disposition)) return refuse(decision.disposition, decision.reason);
|
|
184
|
+
|
|
185
|
+
// A constrained disposition is an allow only if every constraint is genuinely enforced for this operation.
|
|
186
|
+
const constraints = decision.constraints ?? [];
|
|
187
|
+
const controls = decision.compensatingControls ?? [];
|
|
188
|
+
if (constraints.length === 0 || !decision.policyBasis?.length) return refuse("unknown", `${decision.disposition} arrived without constraints and policy basis`);
|
|
189
|
+
if (decision.disposition === "degraded" && controls.length === 0) return refuse("unknown", "degraded arrived without compensating controls");
|
|
190
|
+
const enforcers = this.options.enforcers ?? {};
|
|
191
|
+
const applied: GovernanceConstraint[] = [];
|
|
192
|
+
const needed: { key: string; constraint: GovernanceConstraint }[] = [
|
|
193
|
+
...constraints.map((constraint) => ({ key: constraint.type, constraint })),
|
|
194
|
+
...(decision.disposition === "degraded" ? controls.map((name) => ({ key: `control:${name}`, constraint: { type: `control:${name}`, value: name } })) : []),
|
|
195
|
+
];
|
|
196
|
+
for (const { key, constraint } of needed) {
|
|
197
|
+
const enforcer = Object.hasOwn(enforcers, key) ? enforcers[key] : undefined;
|
|
198
|
+
if (!enforcer) return refuse(decision.disposition, `${decision.disposition} requires '${key}' and no enforcer is registered: ${decision.reason}`);
|
|
199
|
+
let enforced = false;
|
|
200
|
+
try {
|
|
201
|
+
enforced = (await enforcer.apply(constraint, operation)) === true;
|
|
202
|
+
} catch {
|
|
203
|
+
enforced = false;
|
|
204
|
+
}
|
|
205
|
+
if (!enforced) return refuse(decision.disposition, `${decision.disposition} constraint '${key}' could not be enforced: ${decision.reason}`);
|
|
206
|
+
applied.push(constraint);
|
|
207
|
+
}
|
|
208
|
+
return { allow: true, mode, disposition: decision.disposition, reason: decision.reason, applied, ...ref };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Kernel-policy facade: the same decision, shaped as the kernel expects. `onApplied` receives the
|
|
213
|
+
* constraints that were enforced before the allow, so the invoker can put them in force for the call.
|
|
214
|
+
*/
|
|
215
|
+
async governCapability(request: AuthorizationRequest, hooks: CapabilityHooks = {}): Promise<PolicyDecision> {
|
|
216
|
+
const result = await this.govern({ kind: "capability", request }, hooks.approval);
|
|
217
|
+
if (result.allow && result.applied?.length) hooks.applied?.(result.applied);
|
|
218
|
+
if (!result.allow && result.escalation) hooks.escalated?.(result.escalation);
|
|
219
|
+
// `authority` rides in the recorded decision (run.invocation metadata), so a later trace can name it.
|
|
220
|
+
return { allow: result.allow, policyId: result.mode === "sea-forge" ? "sea-forge" : "local", reason: result.reason, ...(result.authority ? { authority: result.authority } : {}) } as PolicyDecision;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const KNOWN: ReadonlySet<string> = new Set<Disposition>(["allow", "deny", "escalate", "unknown", "constrained_allow", "boundary", "degraded"]);
|
|
225
|
+
const CONSTRAINED: ReadonlySet<string> = new Set<Disposition>(["constrained_allow", "boundary", "degraded"]);
|
|
226
|
+
|
|
227
|
+
/** Largest timeout an authority may impose; a longer one is not a meaningful bound. */
|
|
228
|
+
export const MAX_TIMEOUT_SECONDS = 3600;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Enforcer for the `timeout_secs` constraint. It reports "enforced" only for capability invocations, because
|
|
232
|
+
* only the Invoker puts a deadline in force (it reads the applied constraint, aborts the provider's signal and
|
|
233
|
+
* fails the call with `deadline_exceeded`). An action has no such deadline, so the constraint cannot be honored
|
|
234
|
+
* there and the operation is refused. Register it under `enforcers: { timeout_secs: timeoutSecsEnforcer }`.
|
|
235
|
+
*/
|
|
236
|
+
export const timeoutSecsEnforcer: ConstraintEnforcer = {
|
|
237
|
+
apply(constraint, operation) {
|
|
238
|
+
if (operation.kind !== "capability") return false;
|
|
239
|
+
return parseTimeoutSeconds(constraint.value) !== undefined;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** A whole number of seconds in 1..=MAX_TIMEOUT_SECONDS, else undefined. */
|
|
244
|
+
export function parseTimeoutSeconds(value: string): number | undefined {
|
|
245
|
+
if (!/^[1-9][0-9]{0,3}$/.test(value)) return undefined;
|
|
246
|
+
const seconds = Number(value);
|
|
247
|
+
return seconds <= MAX_TIMEOUT_SECONDS ? seconds : undefined;
|
|
248
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CognitiveIndex, Embedder, IndexDocument } from "@godspeedai/cognate-cognitive-api";
|
|
2
|
+
import type { DurableStore, Json, NewEvent, OutboxIntent, StoredEvent } from "@godspeedai/cognate-events";
|
|
3
|
+
import type { Component, Kernel, Policy, Principal } from "@godspeedai/cognate-kernel-api";
|
|
4
|
+
import type { Projection } from "@godspeedai/cognate-projections";
|
|
5
|
+
import type { AgentDefinition, Caller, RuntimeService } from "@godspeedai/cognate-runtime-api";
|
|
6
|
+
import type { GovernanceOptions } from "./governed-operation.ts";
|
|
7
|
+
import { startRuntime } from "./runtime.ts";
|
|
8
|
+
|
|
9
|
+
export { SeaForgeAuthority, buildAuthorityRequest, buildTransitionRequest, interpretDecision, interpretSettlement, type CarriedWork, type EvidenceReceipt, type RecordedTransition, type SeaForgeAuthorityOptions, type SettlementReceipt, type WorldTransitionRequest } from "./sea-forge-authority.ts";
|
|
10
|
+
export { ENV_ACTOR, ENV_MODE, ENV_SOCKET, ENV_WORLD, governanceFromEnv } from "./governance-env.ts";
|
|
11
|
+
export { Governor, isWorldRef, parseTimeoutSeconds, timeoutSecsEnforcer, validateGovernance } from "./governed-operation.ts";
|
|
12
|
+
export type { ApprovalReference, AuthorityReference, ApprovalState, CapabilityHooks, ConstraintEnforcer, GovernanceEscalation, Disposition, GovernanceAuthority, GovernanceConstraint, GovernanceDecision, GovernanceMode, GovernanceOptions, GovernedOperation, GovernedResult } from "./governed-operation.ts";
|
|
13
|
+
|
|
14
|
+
export interface ActionPolicy {
|
|
15
|
+
/** Authorize run and continuation actions (start, cancel, resume, read artifacts). Default: deny (spec §32). */
|
|
16
|
+
authorize(request: { readonly caller: Caller; readonly action: "run.start" | "run.cancel" | "continuation.resume" | "continuation.cancel" | "run.state.update" | "thread.state.read" | "thread.state.update" | "observation.record" | "remote.offer.publish" | "remote.offer.list" | "remote.offer.revoke" | "remote.offer.invoke"; readonly resource: string }): { readonly allow: boolean; readonly reason: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CognitionOptions {
|
|
20
|
+
readonly index: CognitiveIndex;
|
|
21
|
+
readonly embedder: Embedder;
|
|
22
|
+
/** Extract documents to index from committed events; defaults to indexing completed runs. */
|
|
23
|
+
readonly extract?: (event: StoredEvent) => readonly IndexDocument[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether the index survives restart with its data. When false (memory, RuVector adapter), the runtime
|
|
26
|
+
* rebuilds the cognitive projection on start so the checkpoint never claims entries the index lost.
|
|
27
|
+
*/
|
|
28
|
+
readonly persistent: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A run event about to be committed. */
|
|
32
|
+
export type PendingEvent = NewEvent & { readonly eventId: string; readonly streamId: string; readonly recordedAt: string };
|
|
33
|
+
|
|
34
|
+
/** Outbox topic whose items represent RealityTrace observations; `inspect` reports their observation ids. */
|
|
35
|
+
export const OBSERVATION_OUTBOX_TOPIC = "observation.deliver";
|
|
36
|
+
|
|
37
|
+
export interface RuntimeOptions {
|
|
38
|
+
/** SQLite file path, `":memory:"` for an in-memory SQLite database, or an explicit store. */
|
|
39
|
+
readonly store: string | { readonly store: DurableStore };
|
|
40
|
+
/** Kernel invocation policy for capabilities (default-deny if omitted). */
|
|
41
|
+
readonly policy: Policy;
|
|
42
|
+
readonly actions: ActionPolicy;
|
|
43
|
+
/** Governance of every consequential operation. Default `{ mode: "off" }` (local policy only) until Stage 6 wires SEA-Forge. */
|
|
44
|
+
readonly governance?: GovernanceOptions;
|
|
45
|
+
readonly agents: readonly AgentDefinition[];
|
|
46
|
+
/** Capability-providing components mounted in the root scope. */
|
|
47
|
+
readonly components?: readonly Component[];
|
|
48
|
+
readonly cognition?: CognitionOptions;
|
|
49
|
+
/** Projections kept current by the runtime; `public` ones are readable through the service. */
|
|
50
|
+
readonly projections?: readonly { readonly projection: Projection; readonly public: boolean }[];
|
|
51
|
+
/**
|
|
52
|
+
* Extra outbox intents committed atomically with each run event (spec §13.1), e.g. RealityTrace observation
|
|
53
|
+
* intents. Receives the event before commit: `eventId` is pre-assigned; `recordedAt` is provisional (= occurredAt).
|
|
54
|
+
*/
|
|
55
|
+
readonly outboxFor?: (event: PendingEvent) => readonly OutboxIntent[];
|
|
56
|
+
/** Background work loop interval (run execution, resume delivery, expiry). Default 10 ms. */
|
|
57
|
+
readonly tickMs?: number;
|
|
58
|
+
readonly system?: Principal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CausalChain {
|
|
62
|
+
readonly correlationId: string;
|
|
63
|
+
readonly runs: readonly string[];
|
|
64
|
+
readonly events: readonly { readonly eventId: string; readonly eventType: string; readonly causationId: string | null; readonly componentId?: string; readonly fiberId?: string; readonly semanticRef?: Json }[];
|
|
65
|
+
readonly invocations: readonly { readonly capability: string; readonly providerId: string; readonly fiberId: string; readonly decision: Json }[];
|
|
66
|
+
readonly artifacts: readonly string[];
|
|
67
|
+
readonly observations: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Runtime {
|
|
71
|
+
readonly service: RuntimeService;
|
|
72
|
+
readonly kernel: Kernel;
|
|
73
|
+
readonly store: DurableStore;
|
|
74
|
+
/** Reconstruct machine-readable causal identity for a correlation id (spec §33, §40.M). */
|
|
75
|
+
inspect(correlationId: string): Promise<CausalChain>;
|
|
76
|
+
/** Clear the cognitive index and rebuild it from authoritative events (spec §3.5). */
|
|
77
|
+
rebuildCognition(): Promise<void>;
|
|
78
|
+
/** Erase an event payload under retention/privacy policy; derived views and the cognitive index drop it (spec §32). */
|
|
79
|
+
erase(eventId: string, reason: string, actor: Principal): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* One pass over runs waiting on a governance approval: ask the authority where each approval stands, resume
|
|
82
|
+
* the approved ones and cancel the rejected or expired ones. A resume only triggers revalidation; the
|
|
83
|
+
* authority decides again when the run retries. Returns how many waits it acted on.
|
|
84
|
+
*/
|
|
85
|
+
pollApprovals(): Promise<number>;
|
|
86
|
+
/** Wait until background work queued so far has been processed (tests and graceful shutdown). */
|
|
87
|
+
idle(timeoutMs?: number): Promise<void>;
|
|
88
|
+
close(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createRuntime(options: RuntimeOptions): Promise<Runtime> {
|
|
92
|
+
return startRuntime(options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type { Json };
|
package/src/inspect.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Causal identity reconstruction from committed envelopes (spec §33, §40.M). Uses only envelope fields and
|
|
2
|
+
// metadata, so it still works after payload erasure.
|
|
3
|
+
import type { ContinuationService } from "@godspeedai/cognate-continuations";
|
|
4
|
+
import type { DurableStore, Json, StoredEvent } from "@godspeedai/cognate-events";
|
|
5
|
+
import { field, meta } from "./fold.ts";
|
|
6
|
+
import { type CausalChain, OBSERVATION_OUTBOX_TOPIC } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
const PAGE = 1000;
|
|
9
|
+
|
|
10
|
+
export async function inspectCorrelation(store: DurableStore, continuations: ContinuationService, correlationId: string): Promise<CausalChain> {
|
|
11
|
+
const matching: StoredEvent[] = [];
|
|
12
|
+
for (let position = 0; ; ) {
|
|
13
|
+
const page = await store.readAll({ afterPosition: position, limit: PAGE });
|
|
14
|
+
for (const event of page) if (event.correlationId === correlationId) matching.push(event);
|
|
15
|
+
if (page.length < PAGE) break;
|
|
16
|
+
position = page.at(-1)!.position;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Continuation envelopes do not carry the wait's semantic reference; the continuation record does.
|
|
20
|
+
const continuationRefs = new Map<string, Json>();
|
|
21
|
+
for (const event of matching) {
|
|
22
|
+
if (!event.streamId.startsWith("continuation-") || continuationRefs.has(event.streamId)) continue;
|
|
23
|
+
const record = await continuations.get(event.streamId.slice("continuation-".length));
|
|
24
|
+
continuationRefs.set(event.streamId, (record?.semanticRef ?? null) as unknown as Json);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const runs: string[] = [];
|
|
28
|
+
const invocations: { capability: string; providerId: string; fiberId: string; decision: Json }[] = [];
|
|
29
|
+
const artifacts: string[] = [];
|
|
30
|
+
const events = matching.map((event) => {
|
|
31
|
+
const m = meta(event);
|
|
32
|
+
if (event.eventType === "run.started" && event.runId) runs.push(event.runId);
|
|
33
|
+
const providerId = field<string>(m, "providerId");
|
|
34
|
+
if (event.eventType === "run.invocation" && providerId) {
|
|
35
|
+
invocations.push({ capability: field<string>(m, "capability") ?? "", providerId, fiberId: event.fiberId ?? field<string>(m, "fiberId") ?? providerId, decision: field(m, "decision") });
|
|
36
|
+
}
|
|
37
|
+
const artifactId = field<string>(m, "artifactId");
|
|
38
|
+
if (event.eventType === "run.artifact" && artifactId) artifacts.push(artifactId);
|
|
39
|
+
const semanticRef = (event.semanticRef as unknown as Json | undefined) ?? continuationRefs.get(event.streamId) ?? null;
|
|
40
|
+
return {
|
|
41
|
+
eventId: event.eventId,
|
|
42
|
+
eventType: event.eventType,
|
|
43
|
+
causationId: event.causationId,
|
|
44
|
+
...(event.componentId ? { componentId: event.componentId } : {}),
|
|
45
|
+
...(event.fiberId ? { fiberId: event.fiberId } : {}),
|
|
46
|
+
...(semanticRef !== null ? { semanticRef } : {}),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Observations are outbox intents on the observation topic whose source event belongs to this correlation.
|
|
51
|
+
const eventIds = new Set(matching.map((event) => event.eventId));
|
|
52
|
+
const observations = (await store.listOutbox({ topic: OBSERVATION_OUTBOX_TOPIC }))
|
|
53
|
+
.map((item) => item.payload as { eventId?: Json; observationId?: Json } | null)
|
|
54
|
+
.filter((payload): payload is { eventId: string; observationId: string } => typeof payload?.eventId === "string" && typeof payload.observationId === "string" && eventIds.has(payload.eventId))
|
|
55
|
+
.map((payload) => payload.observationId);
|
|
56
|
+
|
|
57
|
+
return { correlationId, runs, events, invocations, artifacts, observations };
|
|
58
|
+
}
|
package/src/invoker.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Capability invocation through the kernel, with the caller's tenant and the policy decision captured per call.
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import type { Json } from "@godspeedai/cognate-events";
|
|
4
|
+
import { type InvocationResult, isUnavailable } from "@godspeedai/cognate-kernel-api";
|
|
5
|
+
import { type GovernanceEscalation, type Governor, parseTimeoutSeconds } from "./governed-operation.ts";
|
|
6
|
+
import type { Component, ComponentHandle, InvocationContext, InvocationFailure, Kernel, Policy, PolicyDecision, SemanticRef } from "@godspeedai/cognate-kernel-api";
|
|
7
|
+
import { messageOf, toJson } from "./fold.ts";
|
|
8
|
+
|
|
9
|
+
export interface InvocationOutcome {
|
|
10
|
+
readonly capability: string;
|
|
11
|
+
readonly providerId: string | null;
|
|
12
|
+
readonly componentId: string | null;
|
|
13
|
+
readonly fiberId: string | null;
|
|
14
|
+
readonly decision: PolicyDecision | null;
|
|
15
|
+
readonly semanticRef: SemanticRef | null;
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly value: Json;
|
|
18
|
+
/** JSON form of the typed failure, e.g. `{ kind: "denied", reason }`. For "failed" kind, includes `name` (error constructor name) and `details` (error's enumerable properties as string/number/boolean/null). */
|
|
19
|
+
readonly failure: { readonly kind: InvocationFailure["kind"]; readonly [key: string]: Json } | null;
|
|
20
|
+
/** Present when the call was refused because a human approval could satisfy it; the provider did not run. */
|
|
21
|
+
readonly escalation?: GovernanceEscalation;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Slot {
|
|
25
|
+
decision?: PolicyDecision;
|
|
26
|
+
/** The approval being relied on, when this call revalidates an approved escalation. */
|
|
27
|
+
approval?: { readonly approvalId: string };
|
|
28
|
+
/** Set by the policy wrapper when authorization escalated to a human approval. */
|
|
29
|
+
escalation?: GovernanceEscalation;
|
|
30
|
+
/** Puts a governed deadline in force for this call (set by `invoke`, called by the policy wrapper). */
|
|
31
|
+
arm?: (milliseconds: number) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Invoker {
|
|
35
|
+
private readonly slots = new AsyncLocalStorage<Slot>();
|
|
36
|
+
private readonly mounted: { readonly component: Component; readonly handle: ComponentHandle }[] = [];
|
|
37
|
+
private kernel: Kernel | undefined;
|
|
38
|
+
|
|
39
|
+
constructor(private readonly governor: Governor) {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The policy handed to the kernel. The caller's tenant travels in the InvocationContext; this wrapper only
|
|
43
|
+
* captures the decision so it can be recorded with the invocation (spec §32, §33).
|
|
44
|
+
*/
|
|
45
|
+
readonly policy: Policy = {
|
|
46
|
+
authorize: async (request) => {
|
|
47
|
+
const slot = this.slots.getStore();
|
|
48
|
+
const decision = await this.governor.governCapability(request, {
|
|
49
|
+
...(slot?.approval ? { approval: slot.approval } : {}),
|
|
50
|
+
applied: (applied) => {
|
|
51
|
+
const seconds = applied.map((c) => (c.type === "timeout_secs" ? parseTimeoutSeconds(c.value) : undefined)).filter((n): n is number => n !== undefined);
|
|
52
|
+
if (seconds.length && slot?.arm) slot.arm(Math.min(...seconds) * 1000);
|
|
53
|
+
},
|
|
54
|
+
escalated: (escalation) => {
|
|
55
|
+
if (slot) slot.escalation = escalation;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (slot) slot.decision = decision;
|
|
59
|
+
return decision;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async attach(kernel: Kernel, components: readonly Component[]): Promise<void> {
|
|
64
|
+
this.kernel = kernel;
|
|
65
|
+
for (const component of components) {
|
|
66
|
+
const handle = await kernel.mount(component);
|
|
67
|
+
this.mounted.push({ component, handle });
|
|
68
|
+
}
|
|
69
|
+
// A component that fails to activate leaves its capabilities unavailable; it does not stop the runtime.
|
|
70
|
+
await Promise.all(this.mounted.map(({ handle }) => handle.waitFor("ACTIVE", 10_000).catch(() => {})));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async invoke(request: { readonly capability: string; readonly version?: string; readonly input: Json; readonly tenant: string; readonly context: InvocationContext; readonly approval?: { readonly approvalId: string } }): Promise<InvocationOutcome> {
|
|
74
|
+
const lease = this.kernel!.resolve(request.version ? { capability: request.capability, version: request.version } : { capability: request.capability });
|
|
75
|
+
const base = { capability: request.capability, providerId: null, componentId: null, fiberId: null, decision: null, semanticRef: null, value: null };
|
|
76
|
+
if (isUnavailable(lease)) return { ...base, ok: false, failure: { kind: "unavailable", reason: lease.reason } };
|
|
77
|
+
|
|
78
|
+
const slot: Slot = request.approval ? { approval: request.approval } : {};
|
|
79
|
+
// The call gets its own signal so a governed deadline (decided during authorization, after the call has
|
|
80
|
+
// started) can abort the provider; the caller's signal still aborts it too.
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const upstream = request.context.signal;
|
|
83
|
+
if (upstream) {
|
|
84
|
+
if (upstream.aborted) controller.abort(upstream.reason);
|
|
85
|
+
else upstream.addEventListener("abort", () => controller.abort(upstream.reason), { once: true });
|
|
86
|
+
}
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
88
|
+
const governedDeadline = new Promise<InvocationResult<unknown>>((resolve) => {
|
|
89
|
+
slot.arm = (milliseconds) => {
|
|
90
|
+
timer = setTimeout(() => {
|
|
91
|
+
controller.abort(new Error("governed timeout_secs exceeded"));
|
|
92
|
+
resolve({ ok: false, failure: { kind: "deadline_exceeded" } });
|
|
93
|
+
}, milliseconds);
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
const invocation = this.slots.run(slot, () => lease.invoke(request.input, { ...request.context, tenant: request.tenant, signal: controller.signal }));
|
|
97
|
+
invocation.catch(() => {});
|
|
98
|
+
const result = await Promise.race([invocation, governedDeadline]).finally(() => clearTimeout(timer));
|
|
99
|
+
const provider = this.providerOf(lease.providerId, request.capability);
|
|
100
|
+
const common = { ...base, providerId: lease.providerId, ...provider, semanticRef: lease.contract.semanticRef ?? null, ...(slot.escalation ? { escalation: slot.escalation } : {}) };
|
|
101
|
+
if (result.ok) return { ...common, decision: slot.decision ?? null, ok: true, value: toJson(result.value), failure: null };
|
|
102
|
+
const failure = result.failure;
|
|
103
|
+
const decision = failure.kind === "denied" ? failure.decision : (slot.decision ?? null);
|
|
104
|
+
return { ...common, decision, ok: false, failure: failureJson(failure) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Provider fiber and component, from mounted handles (kernel-api only; no engine types). */
|
|
108
|
+
private providerOf(providerId: string, capability: string): { componentId: string | null; fiberId: string | null } {
|
|
109
|
+
const byFiber = this.mounted.find(({ handle }) => handle.fiberId === providerId);
|
|
110
|
+
const match = byFiber ?? this.mounted.find(({ component }) => component.provides?.some((c) => c.id === capability));
|
|
111
|
+
return { componentId: match?.component.id ?? null, fiberId: match?.handle.fiberId ?? providerId };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function failureJson(failure: InvocationFailure): NonNullable<InvocationOutcome["failure"]> {
|
|
116
|
+
switch (failure.kind) {
|
|
117
|
+
case "unavailable":
|
|
118
|
+
return { kind: failure.kind, reason: failure.reason };
|
|
119
|
+
case "denied":
|
|
120
|
+
return { kind: failure.kind, policyId: failure.decision.policyId, reason: failure.decision.reason };
|
|
121
|
+
case "failed": {
|
|
122
|
+
const result: Record<string, Json> = {
|
|
123
|
+
kind: failure.kind,
|
|
124
|
+
message: messageOf(failure.error),
|
|
125
|
+
};
|
|
126
|
+
if (failure.error instanceof Error) {
|
|
127
|
+
result.name = failure.error.name;
|
|
128
|
+
const details: Record<string, Json> = {};
|
|
129
|
+
for (const [key, value] of Object.entries(failure.error)) {
|
|
130
|
+
if (key !== "message" && key !== "stack") {
|
|
131
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
132
|
+
details[key] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(details).length > 0) {
|
|
137
|
+
result.details = details;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result as NonNullable<InvocationOutcome["failure"]>;
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return { kind: failure.kind };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Ephemeral remote capability availability. Durable work starts only after an offer is selected
|
|
2
|
+
// and represented by a normal continuation; offers themselves must not enter the event log.
|
|
3
|
+
import type { Json } from "@godspeedai/cognate-events";
|
|
4
|
+
import type { Caller, RemoteCapabilityOffer } from "@godspeedai/cognate-runtime-api";
|
|
5
|
+
|
|
6
|
+
type StoredOffer = RemoteCapabilityOffer & { readonly tenant: string; readonly actorId: string };
|
|
7
|
+
|
|
8
|
+
export class RemoteOfferRegistry {
|
|
9
|
+
private readonly offers = new Map<string, StoredOffer>();
|
|
10
|
+
|
|
11
|
+
publish(caller: Caller, scopeId: string, request: Omit<RemoteCapabilityOffer, "id" | "scopeId" | "expiresAt"> & { readonly ttlMs: number }): RemoteCapabilityOffer {
|
|
12
|
+
if (!request.leaseId || !request.agent || !request.contract.id || !request.contract.version || !request.description) throw new Error("remote offer requires lease, agent, contract, and description");
|
|
13
|
+
if (!Number.isFinite(request.ttlMs) || request.ttlMs <= 0 || request.ttlMs > 300_000) throw new Error("remote offer ttlMs must be between 1 and 300000");
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
// A lease identifies one mounted client surface. Re-publish replaces its prior declaration for
|
|
16
|
+
// this contract and agent, so a remount cannot leave an old schema eligible.
|
|
17
|
+
const prior = [...this.offers.values()].find((offer) => offer.tenant === caller.tenant && offer.actorId === caller.actor.id && offer.leaseId === request.leaseId && offer.agent === request.agent && offer.contract.id === request.contract.id);
|
|
18
|
+
const offer: StoredOffer = {
|
|
19
|
+
id: prior?.id ?? crypto.randomUUID(), leaseId: request.leaseId, agent: request.agent, scopeId,
|
|
20
|
+
contract: { id: request.contract.id, version: request.contract.version, ...(request.contract.attributes ? { attributes: request.contract.attributes } : {}) },
|
|
21
|
+
description: request.description, inputSchema: request.inputSchema, ...(request.outputSchema === undefined ? {} : { outputSchema: request.outputSchema }),
|
|
22
|
+
expiresAt: now + request.ttlMs, tenant: caller.tenant, actorId: caller.actor.id,
|
|
23
|
+
};
|
|
24
|
+
this.offers.set(offer.id, offer);
|
|
25
|
+
return view(offer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
list(caller: Caller, agent: string): readonly RemoteCapabilityOffer[] {
|
|
29
|
+
this.expire();
|
|
30
|
+
return [...this.offers.values()]
|
|
31
|
+
.filter((offer) => offer.tenant === caller.tenant && offer.actorId === caller.actor.id && offer.agent === agent)
|
|
32
|
+
.sort((a, b) => a.contract.id.localeCompare(b.contract.id) || a.id.localeCompare(b.id))
|
|
33
|
+
.map(view);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
take(caller: Caller, agent: string, offerId: string): RemoteCapabilityOffer | undefined {
|
|
37
|
+
this.expire();
|
|
38
|
+
const offer = this.offers.get(offerId);
|
|
39
|
+
return offer && offer.tenant === caller.tenant && offer.actorId === caller.actor.id && offer.agent === agent ? view(offer) : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
revoke(caller: Caller, offerId: string): boolean {
|
|
43
|
+
const offer = this.offers.get(offerId);
|
|
44
|
+
if (!offer || offer.tenant !== caller.tenant || offer.actorId !== caller.actor.id) return false;
|
|
45
|
+
this.offers.delete(offerId);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private expire(): void {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
for (const [id, offer] of this.offers) if (offer.expiresAt <= now) this.offers.delete(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function view(offer: StoredOffer): RemoteCapabilityOffer {
|
|
56
|
+
const { tenant: _tenant, actorId: _actorId, ...result } = offer;
|
|
57
|
+
return result as RemoteCapabilityOffer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type RemoteOfferInput = { readonly leaseId: string; readonly agent: string; readonly contract: { readonly id: string; readonly version: string; readonly attributes?: Readonly<Record<string, Json>> }; readonly description: string; readonly inputSchema: Json; readonly outputSchema?: Json; readonly ttlMs: number };
|