@mnmal-ai/cortext 1.0.0-alpha.11 → 1.0.0-alpha.12

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/bootstrap.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as isMonotonicAuth, T as DEFAULT_AUTH, t as createCortextPlugin } from "./src-DJA6hyG-.js";
1
+ import { D as isMonotonicAuth, T as DEFAULT_AUTH, t as createCortextPlugin } from "./src-4GYqbw6S.js";
2
2
  import { A as CORTEXT_NAMESPACE, O as ARCHITECTURE_NAMESPACE } from "./plugin-BtBVkZpZ.js";
3
3
  import "./kernel.js";
4
4
  import { defineBootstrap } from "@mnmal-ai/substrate/sdk";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as registerCompactRoute, D as isMonotonicAuth, E as buildAuthPolicies, S as buildClaudeNamespace, T as DEFAULT_AUTH, _ as TASK_STATUS_VOCAB, a as classSatisfies, b as TODO_STATUS_IMPLIES, c as AGENT_RULE_CATEGORY_VOCAB, d as ARCHITECTURE_TYPE_NAMES, f as CLAUDE_TYPE_NAMES, g as HOST_INSTALL_FRAGMENT_STATES, h as HOST_INSTALL_CHECK_STATES, i as bumpClass, l as AGENT_RULE_SEVERITY_ALIASES, m as DECISION_STATUS_VOCAB, n as inferTodoResolution, o as classifySurfaceChange, p as DECISION_STATUS_ALIASES, r as buildReleaseSurface, s as registerOnboardRoute, t as createCortextPlugin, u as AGENT_RULE_SEVERITY_VOCAB, v as TODO_RESOLUTION_ALIASES, w as runCompact, x as buildArchitectureNamespace, y as TODO_STATUS_ALIASES } from "./src-DJA6hyG-.js";
1
+ import { C as registerCompactRoute, D as isMonotonicAuth, E as buildAuthPolicies, S as buildClaudeNamespace, T as DEFAULT_AUTH, _ as TASK_STATUS_VOCAB, a as classSatisfies, b as TODO_STATUS_IMPLIES, c as AGENT_RULE_CATEGORY_VOCAB, d as ARCHITECTURE_TYPE_NAMES, f as CLAUDE_TYPE_NAMES, g as HOST_INSTALL_FRAGMENT_STATES, h as HOST_INSTALL_CHECK_STATES, i as bumpClass, l as AGENT_RULE_SEVERITY_ALIASES, m as DECISION_STATUS_VOCAB, n as inferTodoResolution, o as classifySurfaceChange, p as DECISION_STATUS_ALIASES, r as buildReleaseSurface, s as registerOnboardRoute, t as createCortextPlugin, u as AGENT_RULE_SEVERITY_VOCAB, v as TODO_RESOLUTION_ALIASES, w as runCompact, x as buildArchitectureNamespace, y as TODO_STATUS_ALIASES } from "./src-4GYqbw6S.js";
2
2
  import { _ as AGENT_MESSAGE_STATUS_VOCAB } from "./plugin-BtBVkZpZ.js";
3
3
  import "./kernel.js";
4
4
 
@@ -11,7 +11,15 @@
11
11
  * An operator is an identity granted `admin` (or `*`) in this namespace.
12
12
  */
13
13
  import type { NamespaceInstallContext, RegistryEntryResolver, RequestContext } from '@mnmal-ai/substrate/sdk';
14
- /** Whether the caller holds `admin` (or `*`) in the namespace it is calling. */
14
+ /**
15
+ * Whether `identity` is an operator: it holds `admin` (or `*`) in `namespace`,
16
+ * or the deployment admin capability (`SUBSTRATE_ADMIN_CAPABILITY`, default
17
+ * `admin`) in `_system`, as substrate's own admin gate reads it.
18
+ */
19
+ export declare function isOperatorIdentity(identity: RequestContext['identity'], namespace: string, env?: {
20
+ SUBSTRATE_ADMIN_CAPABILITY?: string;
21
+ }): boolean;
22
+ /** Whether the caller is an operator in the namespace it is calling. */
15
23
  export declare function isOperator(ctx: RequestContext): boolean;
16
24
  /**
17
25
  * AgentMessage update/delete: the sender, the recipient, or an operator.
@@ -1,5 +1,5 @@
1
1
  import { A as CORTEXT_NAMESPACE, O as ARCHITECTURE_NAMESPACE, _ as AGENT_MESSAGE_STATUS_VOCAB, b as TODO_RESOLUTION_VOCAB, j as CORTEXT_PROMOTED_COLUMNS, k as CORTEXT_COORDINATOR_NAME, n as SUBSTRATE_CONTRACT_RANGE, v as TODO_LIFECYCLE_VOCAB, y as TODO_PRIORITY_VOCAB } from "./plugin-BtBVkZpZ.js";
2
- import { JSON_HEADERS, RESERVED_AUTOCRUD_FIELDS, ResolverFrameError, and, createResolverWithEdges, defineNamespace, deleteResolver, denyAll, denyVerbs, field, formatQualifiedKey, getResolver, grantOf, grantedIn, jsonResponse, listResolver, makeCtx, or, queryAll, requireIdentity, resolveEntityRow, resolveIdentity, selectAll, updateResolver } from "@mnmal-ai/substrate/sdk";
2
+ import { JSON_HEADERS, RESERVED_AUTOCRUD_FIELDS, ResolverFrameError, SYSTEM_NAMESPACE, and, createResolverWithEdges, defineNamespace, deleteResolver, denyAll, denyVerbs, field, formatQualifiedKey, getResolver, grantOf, grantedIn, jsonResponse, listResolver, makeCtx, or, queryAll, requireIdentity, resolveEntityRow, resolveIdentity, selectAll, updateResolver } from "@mnmal-ai/substrate/sdk";
3
3
 
4
4
  //#region src/auth-policy.ts
5
5
  /**
@@ -40,6 +40,154 @@ function buildAuthPolicies(auth) {
40
40
  return [];
41
41
  }
42
42
 
43
+ //#endregion
44
+ //#region src/participants.ts
45
+ /**
46
+ * Whether `identity` is an operator: it holds `admin` (or `*`) in `namespace`,
47
+ * or the deployment admin capability (`SUBSTRATE_ADMIN_CAPABILITY`, default
48
+ * `admin`) in `_system`, as substrate's own admin gate reads it.
49
+ */
50
+ function isOperatorIdentity(identity, namespace, env = process.env) {
51
+ const grant = grantOf(identity);
52
+ const caps = grantedIn(grant, namespace);
53
+ if (caps.includes("admin") || caps.includes("*")) return true;
54
+ const adminCap = env.SUBSTRATE_ADMIN_CAPABILITY || "admin";
55
+ const system = grantedIn(grant, SYSTEM_NAMESPACE);
56
+ return system.includes(adminCap) || system.includes("*");
57
+ }
58
+ /** Whether the caller is an operator in the namespace it is calling. */
59
+ function isOperator(ctx) {
60
+ return isOperatorIdentity(ctx.identity, ctx.namespace);
61
+ }
62
+ function rowId(args) {
63
+ const raw = args.params?.id;
64
+ return typeof raw === "string" || typeof raw === "number" ? raw : void 0;
65
+ }
66
+ function callerKid(ctx) {
67
+ const id = ctx.identity?.id;
68
+ return typeof id === "string" && id !== "" ? id : null;
69
+ }
70
+ /**
71
+ * AgentMessage update/delete: the sender, the recipient, or an operator.
72
+ * The sender is the key that signed the send (`_metadata.createdBy`); the
73
+ * recipient is the kid on the addressed directory row. Archiving someone
74
+ * else's mail hides it from their inbox, and deleting it destroys it, so
75
+ * neither is a bystander's call.
76
+ */
77
+ function messageParticipantsOnly(installCtx, name, inner) {
78
+ return async (ctx, args) => {
79
+ const id = rowId(args);
80
+ if (id === void 0 || isOperator(ctx)) return inner(ctx, args);
81
+ const ds = args.dataset ?? installCtx.dataset;
82
+ const message = await resolveEntityRow(ds, "AgentMessage", id);
83
+ if (message === null) return inner(ctx, args);
84
+ const caller = callerKid(ctx);
85
+ const sender = message._metadata?.createdBy?.id;
86
+ let recipient = null;
87
+ if (typeof message.recipient === "string" && message.recipient !== "") {
88
+ const { rows } = await ds.read("AgentIdentity", { id: message.recipient });
89
+ recipient = rows[0]?.kid ?? null;
90
+ }
91
+ if (caller !== null && (caller === sender || caller === recipient)) return inner(ctx, args);
92
+ throw new ResolverFrameError("not_participant", `${name}: only this message's sender ('${String(sender ?? "unknown")}') or recipient ('${String(recipient ?? "unknown")}'), or an operator, may change or delete it`, { id: message.id });
93
+ };
94
+ }
95
+ /**
96
+ * Whether `host` names the machine in `kid`'s `@host` part, ignoring case.
97
+ * A kid with no `@` names no machine, so it may carry no host.
98
+ */
99
+ function hostMatchesKid(host, kid) {
100
+ if (host === void 0 || host === null) return true;
101
+ if (typeof kid !== "string" || !kid.includes("@")) return false;
102
+ return String(host).toLowerCase() === kid.slice(kid.lastIndexOf("@") + 1).toLowerCase();
103
+ }
104
+ function refuseForeignHost(name, host, kid) {
105
+ if (hostMatchesKid(host, kid)) return;
106
+ throw new ResolverFrameError("operator_only", `${name}: 'host' must be the machine in the kid ('${String(kid)}'); only an operator may set another`, { field: "host" });
107
+ }
108
+ /**
109
+ * AgentIdentity create: a non-operator may not file a row whose `host` is a
110
+ * machine other than the one its kid names. The fleet audit trusts `host`
111
+ * (and `roaming` on that host) when it decides who may write a host's
112
+ * HostInstall rows.
113
+ */
114
+ function identityCreateGuard(installCtx, name, inner) {
115
+ return async (ctx, args) => {
116
+ const params = args.params ?? {};
117
+ if (!isOperator(ctx)) refuseForeignHost(name, params.host, params.kid);
118
+ if (typeof params.kid === "string") {
119
+ const { rows } = await (args.dataset ?? installCtx.dataset).read("AgentIdentity", {
120
+ where: { kid: { eq: params.kid } },
121
+ limit: 1
122
+ });
123
+ if (rows.length > 0) throw new ResolverFrameError("duplicate_kid", `${name}: an AgentIdentity row for '${params.kid}' already exists; update it instead`, { field: "kid" });
124
+ }
125
+ return inner(ctx, args);
126
+ };
127
+ }
128
+ /**
129
+ * AgentIdentity update: the identity itself (the caller's kid is the row's
130
+ * kid) or an operator. `kid` is the row's identity, so changing it is an
131
+ * operator act even on your own row: otherwise a row could be renamed onto a
132
+ * kid whose mail it would then receive. `host` must stay the kid's machine,
133
+ * and reactivating a retired row is an operator act.
134
+ */
135
+ function identitySelfOnly(installCtx, name, inner) {
136
+ return async (ctx, args) => {
137
+ const id = rowId(args);
138
+ if (id === void 0 || isOperator(ctx)) return inner(ctx, args);
139
+ const ds = args.dataset ?? installCtx.dataset;
140
+ const row = await resolveEntityRow(ds, "AgentIdentity", id);
141
+ if (row === null) return inner(ctx, args);
142
+ const params = args.params ?? {};
143
+ if ("kid" in params && params.kid !== row.kid) throw new ResolverFrameError("operator_only", `${name}: 'kid' is the row's identity and only an operator may change it`, { field: "kid" });
144
+ if (callerKid(ctx) === row.kid) {
145
+ if ("host" in params || "roaming" in params || "project" in params) refuseForeignHost(name, "host" in params ? params.host : row.host, row.kid);
146
+ if (params.active === true && row.active === false) throw new ResolverFrameError("operator_only", `${name}: a retired identity may only be reactivated by an operator`, { field: "active" });
147
+ return inner(ctx, args);
148
+ }
149
+ throw new ResolverFrameError("not_self", `${name}: only '${String(row.kid)}' itself, or an operator, may edit this identity row`, { id: row.id });
150
+ };
151
+ }
152
+ /**
153
+ * Todo create/update: `resolution` says why a CLOSED Todo stopped, so a live
154
+ * Todo carries none. A resolution with a live resulting status is refused,
155
+ * and moving a Todo back to a live status clears the one it had.
156
+ */
157
+ function todoResolutionGuard(installCtx, name, inner, statusAliases) {
158
+ const canon = (v) => {
159
+ const s = String(v).trim().toLowerCase();
160
+ return statusAliases[s] ?? s;
161
+ };
162
+ return async (ctx, args) => {
163
+ const params = { ...args.params ?? {} };
164
+ const statusGiven = params.status !== void 0 && params.status !== null;
165
+ let resulting = statusGiven ? canon(params.status) : null;
166
+ let current = null;
167
+ const id = rowId(args);
168
+ if (id !== void 0) {
169
+ const ds = args.dataset ?? installCtx.dataset;
170
+ current = await resolveEntityRow(ds, "Todo", id);
171
+ if (resulting === null && current !== null) resulting = canon(current.status);
172
+ }
173
+ if (resulting === null) resulting = "open";
174
+ if (resulting === "closed") return inner(ctx, args);
175
+ if (params.resolution !== void 0 && params.resolution !== null) throw new ResolverFrameError("resolution_on_live_todo", `${name}: 'resolution' is for closed Todos; this one would be '${resulting}'`, { field: "resolution" });
176
+ if (statusGiven && current !== null && current.resolution != null && !("resolution" in params)) params.resolution = null;
177
+ return inner(ctx, {
178
+ ...args,
179
+ params
180
+ });
181
+ };
182
+ }
183
+ /** AgentIdentity delete: operators only. Mail hangs off these rows. */
184
+ function operatorOnly(name, inner) {
185
+ return async (ctx, args) => {
186
+ if (isOperator(ctx)) return inner(ctx, args);
187
+ throw new ResolverFrameError("operator_only", `${name}: only an operator may delete an identity row; retire it with active: false instead`, {});
188
+ };
189
+ }
190
+
43
191
  //#endregion
44
192
  //#region src/compact.ts
45
193
  /**
@@ -173,10 +321,9 @@ function parseBody(req) {
173
321
  function registerCompactRoute(router, ctx, retention, namespace, auth = DEFAULT_AUTH) {
174
322
  router.post("/compact", async (req) => {
175
323
  if (auth.writes === "identity") {
176
- const caps = grantedIn(grantOf(req.identity ?? null), namespace);
177
- if (!caps.includes("admin") && !caps.includes("*")) return jsonResponse(403, {
324
+ if (!isOperatorIdentity(req.identity ?? null, namespace)) return jsonResponse(403, {
178
325
  code: "operator_only",
179
- message: `POST /compact archives across every project and needs an operator (admin in '${namespace}')`
326
+ message: `POST /compact archives across every project and needs an operator (admin in '${namespace}', or the deployment admin)`
180
327
  });
181
328
  }
182
329
  const parsed = parseBody(req);
@@ -258,142 +405,6 @@ function reportUncoveredNamespaces(cfg, metric, warn = console.warn) {
258
405
  return uncovered;
259
406
  }
260
407
 
261
- //#endregion
262
- //#region src/participants.ts
263
- /** Whether the caller holds `admin` (or `*`) in the namespace it is calling. */
264
- function isOperator(ctx) {
265
- const caps = grantedIn(grantOf(ctx.identity), ctx.namespace);
266
- return caps.includes("admin") || caps.includes("*");
267
- }
268
- function rowId(args) {
269
- const raw = args.params?.id;
270
- return typeof raw === "string" || typeof raw === "number" ? raw : void 0;
271
- }
272
- function callerKid(ctx) {
273
- const id = ctx.identity?.id;
274
- return typeof id === "string" && id !== "" ? id : null;
275
- }
276
- /**
277
- * AgentMessage update/delete: the sender, the recipient, or an operator.
278
- * The sender is the key that signed the send (`_metadata.createdBy`); the
279
- * recipient is the kid on the addressed directory row. Archiving someone
280
- * else's mail hides it from their inbox, and deleting it destroys it, so
281
- * neither is a bystander's call.
282
- */
283
- function messageParticipantsOnly(installCtx, name, inner) {
284
- return async (ctx, args) => {
285
- const id = rowId(args);
286
- if (id === void 0 || isOperator(ctx)) return inner(ctx, args);
287
- const ds = args.dataset ?? installCtx.dataset;
288
- const message = await resolveEntityRow(ds, "AgentMessage", id);
289
- if (message === null) return inner(ctx, args);
290
- const caller = callerKid(ctx);
291
- const sender = message._metadata?.createdBy?.id;
292
- let recipient = null;
293
- if (typeof message.recipient === "string" && message.recipient !== "") {
294
- const { rows } = await ds.read("AgentIdentity", { id: message.recipient });
295
- recipient = rows[0]?.kid ?? null;
296
- }
297
- if (caller !== null && (caller === sender || caller === recipient)) return inner(ctx, args);
298
- throw new ResolverFrameError("not_participant", `${name}: only this message's sender ('${String(sender ?? "unknown")}') or recipient ('${String(recipient ?? "unknown")}'), or an operator, may change or delete it`, { id: message.id });
299
- };
300
- }
301
- /**
302
- * Whether `host` names the machine in `kid`'s `@host` part, ignoring case.
303
- * A kid with no `@` names no machine, so it may carry no host.
304
- */
305
- function hostMatchesKid(host, kid) {
306
- if (host === void 0 || host === null) return true;
307
- if (typeof kid !== "string" || !kid.includes("@")) return false;
308
- return String(host).toLowerCase() === kid.slice(kid.lastIndexOf("@") + 1).toLowerCase();
309
- }
310
- function refuseForeignHost(name, host, kid) {
311
- if (hostMatchesKid(host, kid)) return;
312
- throw new ResolverFrameError("operator_only", `${name}: 'host' must be the machine in the kid ('${String(kid)}'); only an operator may set another`, { field: "host" });
313
- }
314
- /**
315
- * AgentIdentity create: a non-operator may not file a row whose `host` is a
316
- * machine other than the one its kid names. The fleet audit trusts `host`
317
- * (and `roaming` on that host) when it decides who may write a host's
318
- * HostInstall rows.
319
- */
320
- function identityCreateGuard(installCtx, name, inner) {
321
- return async (ctx, args) => {
322
- const params = args.params ?? {};
323
- if (!isOperator(ctx)) refuseForeignHost(name, params.host, params.kid);
324
- if (typeof params.kid === "string") {
325
- const { rows } = await (args.dataset ?? installCtx.dataset).read("AgentIdentity", {
326
- where: { kid: { eq: params.kid } },
327
- limit: 1
328
- });
329
- if (rows.length > 0) throw new ResolverFrameError("duplicate_kid", `${name}: an AgentIdentity row for '${params.kid}' already exists; update it instead`, { field: "kid" });
330
- }
331
- return inner(ctx, args);
332
- };
333
- }
334
- /**
335
- * AgentIdentity update: the identity itself (the caller's kid is the row's
336
- * kid) or an operator. `kid` is the row's identity, so changing it is an
337
- * operator act even on your own row: otherwise a row could be renamed onto a
338
- * kid whose mail it would then receive. `host` must stay the kid's machine,
339
- * and reactivating a retired row is an operator act.
340
- */
341
- function identitySelfOnly(installCtx, name, inner) {
342
- return async (ctx, args) => {
343
- const id = rowId(args);
344
- if (id === void 0 || isOperator(ctx)) return inner(ctx, args);
345
- const ds = args.dataset ?? installCtx.dataset;
346
- const row = await resolveEntityRow(ds, "AgentIdentity", id);
347
- if (row === null) return inner(ctx, args);
348
- const params = args.params ?? {};
349
- if ("kid" in params && params.kid !== row.kid) throw new ResolverFrameError("operator_only", `${name}: 'kid' is the row's identity and only an operator may change it`, { field: "kid" });
350
- if (callerKid(ctx) === row.kid) {
351
- if ("host" in params || "roaming" in params || "project" in params) refuseForeignHost(name, "host" in params ? params.host : row.host, row.kid);
352
- if (params.active === true && row.active === false) throw new ResolverFrameError("operator_only", `${name}: a retired identity may only be reactivated by an operator`, { field: "active" });
353
- return inner(ctx, args);
354
- }
355
- throw new ResolverFrameError("not_self", `${name}: only '${String(row.kid)}' itself, or an operator, may edit this identity row`, { id: row.id });
356
- };
357
- }
358
- /**
359
- * Todo create/update: `resolution` says why a CLOSED Todo stopped, so a live
360
- * Todo carries none. A resolution with a live resulting status is refused,
361
- * and moving a Todo back to a live status clears the one it had.
362
- */
363
- function todoResolutionGuard(installCtx, name, inner, statusAliases) {
364
- const canon = (v) => {
365
- const s = String(v).trim().toLowerCase();
366
- return statusAliases[s] ?? s;
367
- };
368
- return async (ctx, args) => {
369
- const params = { ...args.params ?? {} };
370
- const statusGiven = params.status !== void 0 && params.status !== null;
371
- let resulting = statusGiven ? canon(params.status) : null;
372
- let current = null;
373
- const id = rowId(args);
374
- if (id !== void 0) {
375
- const ds = args.dataset ?? installCtx.dataset;
376
- current = await resolveEntityRow(ds, "Todo", id);
377
- if (resulting === null && current !== null) resulting = canon(current.status);
378
- }
379
- if (resulting === null) resulting = "open";
380
- if (resulting === "closed") return inner(ctx, args);
381
- if (params.resolution !== void 0 && params.resolution !== null) throw new ResolverFrameError("resolution_on_live_todo", `${name}: 'resolution' is for closed Todos; this one would be '${resulting}'`, { field: "resolution" });
382
- if (statusGiven && current !== null && current.resolution != null && !("resolution" in params)) params.resolution = null;
383
- return inner(ctx, {
384
- ...args,
385
- params
386
- });
387
- };
388
- }
389
- /** AgentIdentity delete: operators only. Mail hangs off these rows. */
390
- function operatorOnly(name, inner) {
391
- return async (ctx, args) => {
392
- if (isOperator(ctx)) return inner(ctx, args);
393
- throw new ResolverFrameError("operator_only", `${name}: only an operator may delete an identity row; retire it with active: false instead`, {});
394
- };
395
- }
396
-
397
408
  //#endregion
398
409
  //#region src/mailbox.ts
399
410
  const TYPE = "AgentMessage";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnmal-ai/cortext",
3
- "version": "1.0.0-alpha.11",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "Cortext: the agent-neutral coordination context store for Substrate (Todos, Decisions, SessionLogs, AgentRules, Memory, Concepts, Councils, …) plus architecture findings. One package carrying two plugins: the coordinator that registers the namespaces, and the kernel plugin that owns their Postgres invariants.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {