@kontextmind/kxm 0.7.49 → 0.7.50
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/.claude-plugin/marketplace.json +1 -1
- package/docs/kb/qa-authentik-authentication.md +52 -19
- package/docs/kb/qa-hub-on-a-public-host.md +1 -1
- package/docs/operations.md +9 -1
- package/docs/operator-pi-packages.md +3 -2
- package/docs/workflow-guide.md +3 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +7 -4
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/memory.ts +2 -2
|
@@ -8,18 +8,24 @@ status: "draft"
|
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-17"
|
|
10
10
|
updated: "2026-09-18"
|
|
11
|
-
authority: "
|
|
12
|
-
confidence: "
|
|
13
|
-
summary: "
|
|
11
|
+
authority: "hypothesis"
|
|
12
|
+
confidence: "uncertain"
|
|
13
|
+
summary: "Authentik authenticates browsers at the reverse proxy and the tenant portal's backend calls the loopback hub with existing machine tokens. KXM does not interpret browser identity headers, and the hub-side JWT verification and token broker once recommended here are rejected, not deferred."
|
|
14
14
|
tags: ["hub", "authentik", "oidc", "auth"]
|
|
15
|
-
related: ["docs/operations.md", "docs/kb/qa-hub-on-a-public-host.md"]
|
|
15
|
+
related: ["docs/operations.md", "docs/kb/qa-hub-on-a-public-host.md", "plans/plan-per-tenant-hosting.md", "plans/implementation-plan.md"]
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# Q&A: Authentik (OIDC) for user/role/agent authentication
|
|
19
19
|
|
|
20
20
|
> Researched by `claude --model fable` (planner, read-only) · 2026-09-17 · task_c0bb05339e15 · root review: pending
|
|
21
21
|
|
|
22
|
-
**Short answer
|
|
22
|
+
**Short answer, as of 2026-09-20: no hub-side identity subsystem at all.** Authentik
|
|
23
|
+
authenticates browsers at the tenant's reverse proxy and the portal's own backend calls the
|
|
24
|
+
loopback hub with the machine tokens that already work. The token-broker and JWT
|
|
25
|
+
recommendations that used to sit in this answer are **rejected**, not deferred; they were
|
|
26
|
+
plausible for a shared multi-tenant hub, which is not what we are deploying. Option 1 below —
|
|
27
|
+
forward-auth at the existing proxy, hub unchanged — is the **selected** shape; options 2 and 3
|
|
28
|
+
are rejections, not stages.
|
|
23
29
|
|
|
24
30
|
## What exists today
|
|
25
31
|
|
|
@@ -35,30 +41,57 @@ The hub knows about three credentials. None of them carries a user identity, a r
|
|
|
35
41
|
|
|
36
42
|
## Integration options
|
|
37
43
|
|
|
38
|
-
### 1. Reverse-proxy forward-auth (Authentik outpost at the proxy; hub unchanged)
|
|
44
|
+
### 1. Reverse-proxy forward-auth (Authentik outpost at the proxy; hub unchanged) — **selected shape at the edge**
|
|
39
45
|
|
|
40
|
-
Authentik's proxy outpost authenticates browser sessions and passes headers upstream. The hub ignores those headers today,
|
|
46
|
+
Authentik's proxy outpost authenticates browser sessions and passes headers upstream. The hub ignores those headers today, and under the per-tenant decision it keeps ignoring
|
|
47
|
+
them: the **portal's own backend** holds the hub bearer server-side and calls loopback, while
|
|
48
|
+
the proxy's job is to keep the hub's routes off the public interface entirely. Do not
|
|
49
|
+
configure the proxy to inject the admin token on behalf of every user — that flattens all
|
|
50
|
+
Authentik users to hub admin. What this option does **not** give the hub is per-user identity
|
|
51
|
+
for logging, which is the portal's to record. Effort: low, config only. Risk: low if the hub keeps its token check; medium if someone sets the proxy to add the admin token for every authenticated user, which flattens all Authentik users to admin. Non-loopback bind already requires a token (`hub.ts:468`), so the proxy cannot make the hub anonymous.
|
|
41
52
|
|
|
42
|
-
### 2. Hub validates Authentik-issued JWTs (OIDC discovery + JWKS)
|
|
53
|
+
### 2. Hub validates Authentik-issued JWTs (OIDC discovery + JWKS) — **rejected 2026-09-20; a new decision is required to revisit**
|
|
43
54
|
|
|
44
55
|
Add a second accepted credential in `bearerToken`'s callers: if the bearer parses as a JWT, verify `iss`, `aud`, `exp`, and signature against a cached JWKS from `<issuer>/.well-known/openid-configuration`; else fall through to the existing `safeTokenEqual` path. Code changes: a new `oidc.ts` (discovery, JWKS cache, verify via `node:crypto` `createPublicKey` from JWK, or add `jose`), new `MeshHubOptions.oidc` and `KXM_OIDC_ISSUER` / `KXM_OIDC_AUDIENCE` env in `server.ts` and `hub-env.ts`, and changes to `requireAdminAuth` and `requireProjectAuth` to accept a verified claim set. Mapping: `groups` claim to admin (e.g. `kxm-admin`) and to project scope (e.g. `kxm-project:<name>`). Client side: `HubClient.authToken` (`client.ts:539`) already sends any string as bearer, so a client can pass an Authentik access token unchanged. Effort: medium, roughly 300 to 500 lines plus tests. Risk: medium. New network dependency at auth time (JWKS fetch must fail closed, never skip), clock skew, and the hub must reject `alg: none` and HS256. Agent registration would gain a real principal to store on the agent record (`sub`, `preferred_username`), which the schema can absorb since records are opaque JSON (`store.ts:26`).
|
|
45
56
|
|
|
46
|
-
### 3. Token-broker mapping (Authentik users/groups mint per-user or per-agent kxm tokens)
|
|
57
|
+
### 3. Token-broker mapping (Authentik users/groups mint per-user or per-agent kxm tokens) — **rejected 2026-09-20; a new decision is required to revisit**
|
|
47
58
|
|
|
48
59
|
A small broker (could be a new hub route or a sidecar) accepts an Authentik ID token, verifies it as in option 2, then issues the tokens the runtime already consumes: a project token entry for `requireProjectAuth`, and a `kxm.session-token.v1` with a `toolPolicy` derived from the user's group (`mintSessionToken`, `commands.ts:944`). Mapping table: Authentik group to kxm role id (`role.ts:63` ids `writer`, `planner`, `critic-*`, `verifier`), role `tools` block to `ToolPolicy`, and group to project list to `projectTokens`. Today project tokens are a static map read at startup (`hub.ts:385`), so per-user project tokens need either a dynamic token store in `MeshStore` or short-lived tokens the hub can look up. Session tokens are unsigned (`commands.ts:977`), so a broker-issued one only means something if `parseSessionToken` gains signature verification; otherwise any local process can forge the same payload. Effort: medium to high, because it touches token storage, signing, and revocation. Risk: medium. Benefit: agents and humans converge on one identity source without changing every hub route.
|
|
49
60
|
|
|
50
|
-
## Recommendation
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
## Recommendation (replaced 2026-09-20)
|
|
62
|
+
|
|
63
|
+
Options **2 and 3**, and the staged recommendation that used to sit in this section, are
|
|
64
|
+
**superseded** — option 1 above is the selected shape, not a rejected one. They stay visible
|
|
65
|
+
because they were the plausible answer for a month and someone will meet them again: **do not build a hub-side JWT verifier, a
|
|
66
|
+
token broker, per-user project tokens, or signed session/attempt token issuance as hosting
|
|
67
|
+
prerequisites.** They were a reasonable answer to "one hub, many users"; they are the wrong
|
|
68
|
+
answer to the deployment we actually have, which is **one tenant per box**.
|
|
69
|
+
|
|
70
|
+
1. **Now:** Authentik authenticates and authorizes browsers at the tenant's existing reverse
|
|
71
|
+
proxy, on the **portal's** routes. The portal's server-side backend calls the loopback hub
|
|
72
|
+
and supervisor using the machine credentials that already work (`KXM_AUTH_TOKEN`,
|
|
73
|
+
`KXM_PROJECT_TOKENS`, the persisted hub env record). The hub binds loopback and exposes no
|
|
74
|
+
public listener. Nothing in `hub.ts` changes, and no browser identity header reaches it.
|
|
75
|
+
2. **What stays as-is, unmodified:** the admin-token requirement for non-loopback binding, the
|
|
76
|
+
generated-and-persisted token, project tokens read at startup, and the local loopback
|
|
77
|
+
convenience. A browser-path outage denies browsers; it must not stop authorized machine
|
|
78
|
+
clients.
|
|
79
|
+
3. **Rejected, not deferred:** hub-side JWT verification, group-to-`ToolPolicy` mapping,
|
|
80
|
+
token signing, dynamic per-user token stores, and OAuth2 client-credentials or
|
|
81
|
+
device-flow login for agents. These are not waiting for a trigger; they are the wrong
|
|
82
|
+
shape for a per-tenant hub, and reopening any of them needs a **new written decision**
|
|
83
|
+
that says what the portal boundary cannot do — for example genuine per-user attribution
|
|
84
|
+
of hub writes inside KXM itself, which per-box tenancy does not need. Until such a
|
|
85
|
+
decision exists, treat every mention of them in this file as history.
|
|
86
|
+
|
|
87
|
+
The technical observations underneath remain accurate and are the reason the option is *cheap to
|
|
88
|
+
reject*: unsigned session tokens (`commands.ts:977`), static project tokens read at startup
|
|
89
|
+
(`hub.ts:385`), a fixed-string `HubClient.authToken`, and a plain-string compare in
|
|
90
|
+
`studio-layout.ts:350`. **One item is kept as a live hardening note, not a hosting
|
|
91
|
+
prerequisite:** make that Studio compare timing-safe.
|
|
59
92
|
|
|
60
93
|
## Agent auth specifically
|
|
61
94
|
|
|
62
95
|
Running agents today authenticate non-interactively with whatever string lands in `HubClient.authToken`, resolved by `resolveClientHubAuthToken` (`hub-env.ts:203`): `KXM_AUTH_TOKEN` env, else the persisted project token, else the persisted admin token. The MCP server (`mcp-server.ts:84`) and the Pi extension (`extension.ts:709`) both use this. Pi worker children inherit `process.env` unchanged (`pi-producer.ts:504`), so they get the same token as the supervisor. Tool policy for workers comes from `KXM_ATTEMPT_TOKEN`, which is minted only in tests today (`test/core/commands-policy.test.ts:60`); no runtime path in `plugins/kxm/src` calls `mintAttemptToken`, so engine issuance is planned but not wired.
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
**Superseded paragraph, kept for the record (see the recommendation above):** with Authentik, agents should use the OAuth2 client-credentials grant (one Authentik application per agent class, or per role such as `writer` and `verifier`), obtain an access token at spawn, and pass it as `KXM_AUTH_TOKEN` to the child. This needs option 2 in the hub so the token verifies, and a refresh hook in `HubClient` since `headers()` reads a fixed string (`client.ts:539`) and access tokens expire. Device-code flow is the fallback for Claude Code sessions that start from a human terminal. Whether Authentik's client-credentials tokens carry `groups` claims by default is unknown from this repo; it must be confirmed against the Authentik provider config before mapping roles from claims.
|
|
@@ -10,7 +10,7 @@ created: "2026-09-17"
|
|
|
10
10
|
updated: "2026-09-18"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "reviewed"
|
|
13
|
-
summary: "One hub is one process and one
|
|
13
|
+
summary: "One hub is one process and one state set per tenant box behind a TLS proxy; it is not multi-tenant and must never be exposed directly to the public internet. Hosting tenancy is the machine plus the portal, and the hub binds loopback."
|
|
14
14
|
tags: ["hub", "deployment", "security"]
|
|
15
15
|
related: ["docs/operations.md", "docs/kb/qa-authentik-authentication.md", "docs/kb/qa-what-the-hub-stores.md"]
|
|
16
16
|
---
|
package/docs/operations.md
CHANGED
|
@@ -146,6 +146,14 @@ Recommended alerts:
|
|
|
146
146
|
|
|
147
147
|
## Backup and restore
|
|
148
148
|
|
|
149
|
+
> **Hub-only today, and labelled as such.** The recipe below stops the hub and copies
|
|
150
|
+
> `.kxm/state/kxm.db`. That is not the whole tenant state set: the Runtime keeps its own
|
|
151
|
+
> `registry.db`, per-project event stores under the user state root, prompt sidecars,
|
|
152
|
+
> bindings and configuration. A restore that follows only these steps can bring the hub back
|
|
153
|
+
> while losing Runtime history. Queue step **S1** replaces this section with a stopped-state
|
|
154
|
+
> procedure covering the full set, and **S5** proves it with one deployed restore before real
|
|
155
|
+
> use; until S1 lands, treat this as the hub database only.
|
|
156
|
+
|
|
149
157
|
SQLite runs in WAL mode. The safest simple backup is a coordinated copy while the hub is stopped:
|
|
150
158
|
|
|
151
159
|
1. Stop the hub gracefully.
|
|
@@ -233,5 +241,5 @@ place from v0.4 databases.
|
|
|
233
241
|
- [What is all stored on the hub?](kb/qa-what-the-hub-stores.md)
|
|
234
242
|
- [Storage engine — SQLite vs DuckDB](kb/qa-sqlite-vs-duckdb.md)
|
|
235
243
|
- [Hub on a public host — multiple users and projects?](kb/qa-hub-on-a-public-host.md)
|
|
236
|
-
- [Authentik
|
|
244
|
+
- [Authentik at the edge: why the hub owns no browser identity](kb/qa-authentik-authentication.md)
|
|
237
245
|
- [Extension install → kxm CLI bootstrap + hub auto-connect](kb/qa-extension-install-and-hub-bootstrap.md)
|
|
@@ -43,8 +43,9 @@ commands for RTK independently of `rtk.ts`.
|
|
|
43
43
|
- The antigravity provider is bundled inside `plugins/kxm` (vendored from
|
|
44
44
|
pi-antigravity, MIT); remove any standalone pi-antigravity Pi extension to
|
|
45
45
|
avoid the double-registration warning. `pi-antigravity` and
|
|
46
|
-
`@tian.zuo/pi-antigravity` are different Pi providers.
|
|
47
|
-
the
|
|
46
|
+
`@tian.zuo/pi-antigravity` are different Pi providers. For Google, this bundled
|
|
47
|
+
provider **is** the admitted route (Tracking → Decided, 2026-09-15); `agy` stays a
|
|
48
|
+
catalog/helper entry rather than the admission path.
|
|
48
49
|
- For KXM development loads, prefer the working tree:
|
|
49
50
|
`pi --no-extensions -e ./plugins/kxm/src/extension.ts`. Add every required
|
|
50
51
|
provider extension with another `-e`; otherwise Pi discovery is disabled.
|
package/docs/workflow-guide.md
CHANGED
|
@@ -30,7 +30,9 @@ Existing claims inside preserved candidate lines are research claims, not dispat
|
|
|
30
30
|
|
|
31
31
|
## Selection Policy
|
|
32
32
|
|
|
33
|
-
Candidate selection is measured per role. Filter stages are optional and are not mandatory Tier-0 gating. Prefer a provider-native authenticated subscription when Tracking says that harness is eligible. For Gemini candidates (`google/*`), the admitted
|
|
33
|
+
Candidate selection is measured per role. Filter stages are optional and are not mandatory Tier-0 gating. Prefer a provider-native authenticated subscription when Tracking says that harness is eligible. For Gemini candidates (`google/*`), the admitted route is the bundled `antigravity` **Pi
|
|
34
|
+
provider** (Tracking → Decided, 2026-09-15) — not the OpenRouter provider id, and not a
|
|
35
|
+
shell-out to `agy`, which stays a catalog/helper entry. Do not change the candidate ids themselves (they are dated research). Evidence and review remain workflow-specific. Both the Fable architecture critic and the Sol CLI critic remain required for this developer assignment runner.
|
|
34
36
|
|
|
35
37
|
### Cost band reference (dated candidates)
|
|
36
38
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.50",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -34732,8 +34732,9 @@ Follow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:
|
|
|
34732
34732
|
[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).
|
|
34733
34733
|
|
|
34734
34734
|
You are the **planner / architecture critic** unless the human explicitly asks
|
|
34735
|
-
you to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`)
|
|
34736
|
-
If \`grok\` is logged out,
|
|
34735
|
+
you to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`) \u2014 a starting
|
|
34736
|
+
rotation, not a sole writer. If \`grok\` is logged out, never bill Grok through another harness;
|
|
34737
|
+
use a relief route Tracking **admits**, or stop and name the limits hit. Your reviews are
|
|
34737
34738
|
artifacts, not hub \`peer-reply\` evidence.
|
|
34738
34739
|
`;
|
|
34739
34740
|
const claudeExisted = existsSync25(claudePath);
|
|
@@ -34747,8 +34748,10 @@ artifacts, not hub \`peer-reply\` evidence.
|
|
|
34747
34748
|
Follow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:
|
|
34748
34749
|
[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).
|
|
34749
34750
|
|
|
34750
|
-
Google
|
|
34751
|
-
|
|
34751
|
+
Google goes through the \`antigravity\` **Pi provider** (Tracking \u2192 Decided,
|
|
34752
|
+
2026-09-15); \`agy\` stays a harness catalog/helper entry, not the admission path.
|
|
34753
|
+
Current admissions come from Tracking and \`kxm harness list\`. Starting rotation
|
|
34754
|
+
remains Grok.
|
|
34752
34755
|
`;
|
|
34753
34756
|
const geminiExisted = existsSync25(geminiPath);
|
|
34754
34757
|
if (updateHarnessDocument(geminiPath, block, geminiHeader)) {
|
|
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17121
17121
|
}
|
|
17122
17122
|
|
|
17123
17123
|
// plugins/kxm/src/mcp-server.ts
|
|
17124
|
-
var VERSION = "0.7.
|
|
17124
|
+
var VERSION = "0.7.50";
|
|
17125
17125
|
var inbox = /* @__PURE__ */ new Map();
|
|
17126
17126
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17127
17127
|
var meshClient;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.50";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -365,7 +365,7 @@ export function syncHarnessMemory(repoRoot: string): { updated: string[]; create
|
|
|
365
365
|
|
|
366
366
|
// 2. CLAUDE.md
|
|
367
367
|
const claudePath = join(root, "CLAUDE.md");
|
|
368
|
-
const claudeHeader = `# KXM (Claude)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nYou are the **planner / architecture critic** unless the human explicitly asks\nyou to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`)
|
|
368
|
+
const claudeHeader = `# KXM (Claude)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nYou are the **planner / architecture critic** unless the human explicitly asks\nyou to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`) — a starting\nrotation, not a sole writer. If \`grok\` is logged out, never bill Grok through another harness;\nuse a relief route Tracking **admits**, or stop and name the limits hit. Your reviews are\nartifacts, not hub \`peer-reply\` evidence.\n`;
|
|
369
369
|
const claudeExisted = existsSync(claudePath);
|
|
370
370
|
if (updateHarnessDocument(claudePath, block, claudeHeader)) {
|
|
371
371
|
if (claudeExisted) updated.push("CLAUDE.md");
|
|
@@ -374,7 +374,7 @@ export function syncHarnessMemory(repoRoot: string): { updated: string[]; create
|
|
|
374
374
|
|
|
375
375
|
// 3. GEMINI.md
|
|
376
376
|
const geminiPath = join(root, "GEMINI.md");
|
|
377
|
-
const geminiHeader = `# KXM (Gemini / Antigravity)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nGoogle
|
|
377
|
+
const geminiHeader = `# KXM (Gemini / Antigravity)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nGoogle goes through the \`antigravity\` **Pi provider** (Tracking \u2192 Decided,\n2026-09-15); \`agy\` stays a harness catalog/helper entry, not the admission path.\nCurrent admissions come from Tracking and \`kxm harness list\`. Starting rotation\nremains Grok.\n`;
|
|
378
378
|
const geminiExisted = existsSync(geminiPath);
|
|
379
379
|
if (updateHarnessDocument(geminiPath, block, geminiHeader)) {
|
|
380
380
|
if (geminiExisted) updated.push("GEMINI.md");
|