@grwnd/openclaw-governance 1.4.0 → 1.5.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/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @grwnd/openclaw-governance
2
+
3
+ [![npm](https://img.shields.io/npm/v/@grwnd/openclaw-governance)](https://www.npmjs.com/package/@grwnd/openclaw-governance)
4
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](../../LICENSE)
5
+
6
+ OpenClaw identity bridge plugin for [@grwnd/pi-governance](https://github.com/Grwnd-AI/pi-governance).
7
+
8
+ Parses OpenClaw session keys (WhatsApp, Discord, Slack, Telegram) and maps channel users to governance roles — so pi-governance enforces the right RBAC policy per user without any manual env var setup.
9
+
10
+ ## How it works
11
+
12
+ ```
13
+ OpenClaw session_start
14
+ → @grwnd/openclaw-governance plugin
15
+ → parse sessionKey "agent:<id>:whatsapp:dm:+15550123"
16
+ → lookup "whatsapp:+15550123" in openclaw-users.yaml
17
+ → write process.env.GRWND_USER, GRWND_ROLE, GRWND_ORG_UNIT
18
+ → @grwnd/pi-governance Pi extension
19
+ → EnvIdentityProvider reads the env vars
20
+ → governance enforced with correct role
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ### 1. Install both packages
26
+
27
+ ```bash
28
+ # Install the governance Pi extension
29
+ pi install npm:@grwnd/pi-governance
30
+
31
+ # Install the OpenClaw identity bridge plugin
32
+ openclaw plugins install @grwnd/openclaw-governance
33
+ ```
34
+
35
+ ### 2. Create a users mapping file
36
+
37
+ Create `openclaw-users.yaml` alongside your OpenClaw config:
38
+
39
+ ```yaml
40
+ users:
41
+ # WhatsApp — key by phone number
42
+ whatsapp:+15550123:
43
+ role: report_author
44
+ org_unit: field-ops
45
+
46
+ # Discord — key by user ID
47
+ discord:428374928374:
48
+ role: analyst
49
+
50
+ # Slack — key by member ID
51
+ slack:U04ABCD1234:
52
+ role: project_lead
53
+ org_unit: engineering
54
+
55
+ # Fallback for unknown users (remove to deny access)
56
+ default:
57
+ role: analyst
58
+ org_unit: default
59
+ ```
60
+
61
+ Keys are `<channel>:<peerId>` — the channel name and the platform-specific user identifier.
62
+
63
+ ### 3. Configure the plugin
64
+
65
+ In your OpenClaw config, point to the users file:
66
+
67
+ ```json
68
+ {
69
+ "plugins": {
70
+ "grwnd-openclaw-governance": {
71
+ "users_file": "./openclaw-users.yaml"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ If `users_file` is omitted, it defaults to `./openclaw-users.yaml` in the current working directory.
78
+
79
+ ### 4. Set up governance rules
80
+
81
+ Create your pi-governance config and rules as normal — see the [pi-governance docs](https://grwnd-ai.github.io/pi-governance/guide/quickstart). The roles you assign in `openclaw-users.yaml` must match roles defined in `governance-rules.yaml`.
82
+
83
+ ### 5. Verify
84
+
85
+ When a WhatsApp user sends a message to your OpenClaw agent, you'll see in the audit log:
86
+
87
+ ```json
88
+ {
89
+ "event": "session_start",
90
+ "userId": "whatsapp:+15550123",
91
+ "role": "report_author",
92
+ "orgUnit": "field-ops"
93
+ }
94
+ ```
95
+
96
+ ## Session key formats
97
+
98
+ | Format | Example |
99
+ | ------ | ---------------------------------------------------- |
100
+ | DM | `agent:<agentId>:<channel>:dm:<peerId>` |
101
+ | Group | `agent:<agentId>:<channel>:group:<groupId>:<peerId>` |
102
+
103
+ The plugin ignores keys it cannot parse (e.g. `agent:<id>:main` for direct operator access), leaving the env vars unset so pi-governance falls through to its next identity provider.
104
+
105
+ ## API
106
+
107
+ The plugin exports its internals for programmatic use:
108
+
109
+ ```typescript
110
+ import { parseSessionKey, loadUsers, lookupUser } from '@grwnd/openclaw-governance';
111
+
112
+ const parsed = parseSessionKey('agent:abc:whatsapp:dm:+15550123');
113
+ // { agentId: 'abc', channel: 'whatsapp', chatType: 'dm', peerId: '+15550123' }
114
+
115
+ const config = loadUsers('./openclaw-users.yaml');
116
+ const user = lookupUser(config, 'whatsapp', '+15550123');
117
+ // { role: 'report_author', org_unit: 'field-ops' }
118
+ ```
119
+
120
+ ## License
121
+
122
+ [Apache-2.0](../../LICENSE)
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/parse-session-key.ts","../src/load-users.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { parseSessionKey } from './parse-session-key.js';\nimport { loadUsers, type UsersConfig } from './load-users.js';\n\nexport { parseSessionKey, type ParsedSessionKey } from './parse-session-key.js';\nexport { loadUsers, lookupUser, type UserEntry, type UsersConfig } from './load-users.js';\n\ninterface PluginConfig {\n users_file?: string;\n}\n\ninterface PluginApi {\n on(event: string, handler: (ctx: { sessionKey?: string }) => void): void;\n}\n\nfunction applyIdentity(usersConfig: UsersConfig, sessionKey: string | undefined): void {\n if (!sessionKey) return;\n\n const parsed = parseSessionKey(sessionKey);\n if (!parsed) return;\n\n const user = usersConfig.users[`${parsed.channel}:${parsed.peerId}`] ?? usersConfig.default;\n if (!user) return;\n\n process.env.GRWND_USER = `${parsed.channel}:${parsed.peerId}`;\n process.env.GRWND_ROLE = user.role;\n if (user.org_unit) {\n process.env.GRWND_ORG_UNIT = user.org_unit;\n }\n}\n\nexport const plugin = {\n id: 'grwnd-openclaw-governance',\n\n configSchema: {\n type: 'object' as const,\n properties: {\n users_file: { type: 'string' as const, default: './openclaw-users.yaml' },\n },\n },\n\n register(api: PluginApi, config: PluginConfig = {}) {\n const usersFile = resolve(config.users_file ?? './openclaw-users.yaml');\n const usersConfig = loadUsers(usersFile);\n\n api.on('session_start', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n\n api.on('message_received', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n },\n};\n","export interface ParsedSessionKey {\n agentId: string;\n channel: string;\n chatType: 'dm' | 'group';\n peerId: string;\n groupId?: string;\n}\n\n/**\n * Parse an OpenClaw session key into its components.\n *\n * Supported formats:\n * agent:<agentId>:<channel>:dm:<peerId>\n * agent:<agentId>:<channel>:group:<groupId>:<peerId>\n *\n * Returns null for unrecognised formats (e.g. \"agent:<id>:main\").\n */\nexport function parseSessionKey(key: string): ParsedSessionKey | null {\n const parts = key.split(':');\n\n if (parts[0] !== 'agent' || parts.length < 5) return null;\n\n const agentId = parts[1]!;\n const channel = parts[2]!;\n const chatType = parts[3];\n\n if (chatType === 'dm' && parts.length === 5) {\n return { agentId, channel, chatType: 'dm', peerId: parts[4]! };\n }\n\n if (chatType === 'group' && parts.length === 6) {\n return { agentId, channel, chatType: 'group', groupId: parts[4]!, peerId: parts[5]! };\n }\n\n return null;\n}\n","import { readFileSync } from 'node:fs';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface UserEntry {\n role: string;\n org_unit?: string;\n}\n\nexport interface UsersConfig {\n users: Record<string, UserEntry>;\n default?: UserEntry;\n}\n\n/** Load and parse an openclaw-users.yaml file. */\nexport function loadUsers(filePath: string): UsersConfig {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = parseYaml(raw) as UsersConfig;\n return {\n users: parsed.users ?? {},\n default: parsed.default,\n };\n}\n\n/** Look up a channel user, falling back to `default`. Returns null if neither matches. */\nexport function lookupUser(\n config: UsersConfig,\n channel: string,\n peerId: string,\n): UserEntry | null {\n const key = `${channel}:${peerId}`;\n return config.users[key] ?? config.default ?? null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAwB;;;ACiBjB,SAAS,gBAAgB,KAAsC;AACpE,QAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,MAAI,MAAM,CAAC,MAAM,WAAW,MAAM,SAAS,EAAG,QAAO;AAErD,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,WAAW,MAAM,CAAC;AAExB,MAAI,aAAa,QAAQ,MAAM,WAAW,GAAG;AAC3C,WAAO,EAAE,SAAS,SAAS,UAAU,MAAM,QAAQ,MAAM,CAAC,EAAG;AAAA,EAC/D;AAEA,MAAI,aAAa,WAAW,MAAM,WAAW,GAAG;AAC9C,WAAO,EAAE,SAAS,SAAS,UAAU,SAAS,SAAS,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,EAAG;AAAA,EACtF;AAEA,SAAO;AACT;;;ACnCA,qBAA6B;AAC7B,kBAAmC;AAa5B,SAAS,UAAU,UAA+B;AACvD,QAAM,UAAM,6BAAa,UAAU,OAAO;AAC1C,QAAM,aAAS,YAAAA,OAAU,GAAG;AAC5B,SAAO;AAAA,IACL,OAAO,OAAO,SAAS,CAAC;AAAA,IACxB,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,WACd,QACA,SACA,QACkB;AAClB,QAAM,MAAM,GAAG,OAAO,IAAI,MAAM;AAChC,SAAO,OAAO,MAAM,GAAG,KAAK,OAAO,WAAW;AAChD;;;AFhBA,SAAS,cAAc,aAA0B,YAAsC;AACrF,MAAI,CAAC,WAAY;AAEjB,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,CAAC,OAAQ;AAEb,QAAM,OAAO,YAAY,MAAM,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM,EAAE,KAAK,YAAY;AACpF,MAAI,CAAC,KAAM;AAEX,UAAQ,IAAI,aAAa,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM;AAC3D,UAAQ,IAAI,aAAa,KAAK;AAC9B,MAAI,KAAK,UAAU;AACjB,YAAQ,IAAI,iBAAiB,KAAK;AAAA,EACpC;AACF;AAEO,IAAM,SAAS;AAAA,EACpB,IAAI;AAAA,EAEJ,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,YAAY;AAAA,MACV,YAAY,EAAE,MAAM,UAAmB,SAAS,wBAAwB;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,SAAS,KAAgB,SAAuB,CAAC,GAAG;AAClD,UAAM,gBAAY,0BAAQ,OAAO,cAAc,uBAAuB;AACtE,UAAM,cAAc,UAAU,SAAS;AAEvC,QAAI,GAAG,iBAAiB,CAAC,QAAQ;AAC/B,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAED,QAAI,GAAG,oBAAoB,CAAC,QAAQ;AAClC,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAAA,EACH;AACF;","names":["parseYaml"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/parse-session-key.ts","../src/load-users.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { parseSessionKey } from './parse-session-key.js';\nimport { loadUsers, type UsersConfig } from './load-users.js';\n\nexport { parseSessionKey, type ParsedSessionKey } from './parse-session-key.js';\nexport { loadUsers, lookupUser, type UserEntry, type UsersConfig } from './load-users.js';\n\ninterface PluginConfig {\n users_file?: string;\n}\n\ninterface PluginApi {\n on(event: string, handler: (ctx: { sessionKey?: string }) => void): void;\n}\n\nfunction applyIdentity(usersConfig: UsersConfig, sessionKey: string | undefined): void {\n if (!sessionKey) return;\n\n const parsed = parseSessionKey(sessionKey);\n if (!parsed) return;\n\n const user = usersConfig.users[`${parsed.channel}:${parsed.peerId}`] ?? usersConfig.default;\n if (!user) return;\n\n process.env.GRWND_USER = `${parsed.channel}:${parsed.peerId}`;\n process.env.GRWND_ROLE = user.role;\n if (user.org_unit) {\n process.env.GRWND_ORG_UNIT = user.org_unit;\n }\n}\n\nexport const plugin = {\n id: 'grwnd-openclaw-governance',\n\n configSchema: {\n type: 'object' as const,\n properties: {\n users_file: { type: 'string' as const, default: './openclaw-users.yaml' },\n },\n },\n\n register(api: PluginApi, config: PluginConfig = {}) {\n const usersFile = resolve(config.users_file ?? './openclaw-users.yaml');\n const usersConfig = loadUsers(usersFile);\n\n api.on('session_start', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n\n api.on('message_received', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n },\n};\n","export interface ParsedSessionKey {\n agentId: string;\n channel: string;\n chatType: 'dm' | 'group';\n peerId: string;\n groupId?: string;\n}\n\n/**\n * Parse an OpenClaw session key into its components.\n *\n * Supported formats:\n * agent:<agentId>:<channel>:dm:<peerId>\n * agent:<agentId>:<channel>:group:<groupId>:<peerId>\n *\n * Returns null for unrecognised formats (e.g. \"agent:<id>:main\").\n */\nexport function parseSessionKey(key: string): ParsedSessionKey | null {\n const parts = key.split(':');\n\n if (parts[0] !== 'agent' || parts.length < 5) return null;\n\n const agentId = parts[1]!;\n const channel = parts[2]!;\n const chatType = parts[3];\n\n if (chatType === 'dm' && parts.length === 5) {\n return { agentId, channel, chatType: 'dm', peerId: parts[4]! };\n }\n\n if (chatType === 'group' && parts.length === 6) {\n return { agentId, channel, chatType: 'group', groupId: parts[4]!, peerId: parts[5]! };\n }\n\n return null;\n}\n","import { readFileSync } from 'node:fs';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface UserEntry {\n role: string;\n org_unit?: string;\n}\n\nexport interface UsersConfig {\n users: Record<string, UserEntry>;\n default?: UserEntry;\n}\n\n/** Load and parse an openclaw-users.yaml file. */\nexport function loadUsers(filePath: string): UsersConfig {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = parseYaml(raw) as UsersConfig;\n return {\n users: parsed.users ?? {},\n default: parsed.default,\n };\n}\n\n/** Look up a channel user, falling back to `default`. Returns null if neither matches. */\nexport function lookupUser(config: UsersConfig, channel: string, peerId: string): UserEntry | null {\n const key = `${channel}:${peerId}`;\n return config.users[key] ?? config.default ?? null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAwB;;;ACiBjB,SAAS,gBAAgB,KAAsC;AACpE,QAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,MAAI,MAAM,CAAC,MAAM,WAAW,MAAM,SAAS,EAAG,QAAO;AAErD,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,WAAW,MAAM,CAAC;AAExB,MAAI,aAAa,QAAQ,MAAM,WAAW,GAAG;AAC3C,WAAO,EAAE,SAAS,SAAS,UAAU,MAAM,QAAQ,MAAM,CAAC,EAAG;AAAA,EAC/D;AAEA,MAAI,aAAa,WAAW,MAAM,WAAW,GAAG;AAC9C,WAAO,EAAE,SAAS,SAAS,UAAU,SAAS,SAAS,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,EAAG;AAAA,EACtF;AAEA,SAAO;AACT;;;ACnCA,qBAA6B;AAC7B,kBAAmC;AAa5B,SAAS,UAAU,UAA+B;AACvD,QAAM,UAAM,6BAAa,UAAU,OAAO;AAC1C,QAAM,aAAS,YAAAA,OAAU,GAAG;AAC5B,SAAO;AAAA,IACL,OAAO,OAAO,SAAS,CAAC;AAAA,IACxB,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,WAAW,QAAqB,SAAiB,QAAkC;AACjG,QAAM,MAAM,GAAG,OAAO,IAAI,MAAM;AAChC,SAAO,OAAO,MAAM,GAAG,KAAK,OAAO,WAAW;AAChD;;;AFZA,SAAS,cAAc,aAA0B,YAAsC;AACrF,MAAI,CAAC,WAAY;AAEjB,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,CAAC,OAAQ;AAEb,QAAM,OAAO,YAAY,MAAM,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM,EAAE,KAAK,YAAY;AACpF,MAAI,CAAC,KAAM;AAEX,UAAQ,IAAI,aAAa,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM;AAC3D,UAAQ,IAAI,aAAa,KAAK;AAC9B,MAAI,KAAK,UAAU;AACjB,YAAQ,IAAI,iBAAiB,KAAK;AAAA,EACpC;AACF;AAEO,IAAM,SAAS;AAAA,EACpB,IAAI;AAAA,EAEJ,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,YAAY;AAAA,MACV,YAAY,EAAE,MAAM,UAAmB,SAAS,wBAAwB;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,SAAS,KAAgB,SAAuB,CAAC,GAAG;AAClD,UAAM,gBAAY,0BAAQ,OAAO,cAAc,uBAAuB;AACtE,UAAM,cAAc,UAAU,SAAS;AAEvC,QAAI,GAAG,iBAAiB,CAAC,QAAQ;AAC/B,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAED,QAAI,GAAG,oBAAoB,CAAC,QAAQ;AAClC,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAAA,EACH;AACF;","names":["parseYaml"]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/parse-session-key.ts","../src/load-users.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { parseSessionKey } from './parse-session-key.js';\nimport { loadUsers, type UsersConfig } from './load-users.js';\n\nexport { parseSessionKey, type ParsedSessionKey } from './parse-session-key.js';\nexport { loadUsers, lookupUser, type UserEntry, type UsersConfig } from './load-users.js';\n\ninterface PluginConfig {\n users_file?: string;\n}\n\ninterface PluginApi {\n on(event: string, handler: (ctx: { sessionKey?: string }) => void): void;\n}\n\nfunction applyIdentity(usersConfig: UsersConfig, sessionKey: string | undefined): void {\n if (!sessionKey) return;\n\n const parsed = parseSessionKey(sessionKey);\n if (!parsed) return;\n\n const user = usersConfig.users[`${parsed.channel}:${parsed.peerId}`] ?? usersConfig.default;\n if (!user) return;\n\n process.env.GRWND_USER = `${parsed.channel}:${parsed.peerId}`;\n process.env.GRWND_ROLE = user.role;\n if (user.org_unit) {\n process.env.GRWND_ORG_UNIT = user.org_unit;\n }\n}\n\nexport const plugin = {\n id: 'grwnd-openclaw-governance',\n\n configSchema: {\n type: 'object' as const,\n properties: {\n users_file: { type: 'string' as const, default: './openclaw-users.yaml' },\n },\n },\n\n register(api: PluginApi, config: PluginConfig = {}) {\n const usersFile = resolve(config.users_file ?? './openclaw-users.yaml');\n const usersConfig = loadUsers(usersFile);\n\n api.on('session_start', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n\n api.on('message_received', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n },\n};\n","export interface ParsedSessionKey {\n agentId: string;\n channel: string;\n chatType: 'dm' | 'group';\n peerId: string;\n groupId?: string;\n}\n\n/**\n * Parse an OpenClaw session key into its components.\n *\n * Supported formats:\n * agent:<agentId>:<channel>:dm:<peerId>\n * agent:<agentId>:<channel>:group:<groupId>:<peerId>\n *\n * Returns null for unrecognised formats (e.g. \"agent:<id>:main\").\n */\nexport function parseSessionKey(key: string): ParsedSessionKey | null {\n const parts = key.split(':');\n\n if (parts[0] !== 'agent' || parts.length < 5) return null;\n\n const agentId = parts[1]!;\n const channel = parts[2]!;\n const chatType = parts[3];\n\n if (chatType === 'dm' && parts.length === 5) {\n return { agentId, channel, chatType: 'dm', peerId: parts[4]! };\n }\n\n if (chatType === 'group' && parts.length === 6) {\n return { agentId, channel, chatType: 'group', groupId: parts[4]!, peerId: parts[5]! };\n }\n\n return null;\n}\n","import { readFileSync } from 'node:fs';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface UserEntry {\n role: string;\n org_unit?: string;\n}\n\nexport interface UsersConfig {\n users: Record<string, UserEntry>;\n default?: UserEntry;\n}\n\n/** Load and parse an openclaw-users.yaml file. */\nexport function loadUsers(filePath: string): UsersConfig {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = parseYaml(raw) as UsersConfig;\n return {\n users: parsed.users ?? {},\n default: parsed.default,\n };\n}\n\n/** Look up a channel user, falling back to `default`. Returns null if neither matches. */\nexport function lookupUser(\n config: UsersConfig,\n channel: string,\n peerId: string,\n): UserEntry | null {\n const key = `${channel}:${peerId}`;\n return config.users[key] ?? config.default ?? null;\n}\n"],"mappings":";AAAA,SAAS,eAAe;;;ACiBjB,SAAS,gBAAgB,KAAsC;AACpE,QAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,MAAI,MAAM,CAAC,MAAM,WAAW,MAAM,SAAS,EAAG,QAAO;AAErD,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,WAAW,MAAM,CAAC;AAExB,MAAI,aAAa,QAAQ,MAAM,WAAW,GAAG;AAC3C,WAAO,EAAE,SAAS,SAAS,UAAU,MAAM,QAAQ,MAAM,CAAC,EAAG;AAAA,EAC/D;AAEA,MAAI,aAAa,WAAW,MAAM,WAAW,GAAG;AAC9C,WAAO,EAAE,SAAS,SAAS,UAAU,SAAS,SAAS,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,EAAG;AAAA,EACtF;AAEA,SAAO;AACT;;;ACnCA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,iBAAiB;AAa5B,SAAS,UAAU,UAA+B;AACvD,QAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO;AAAA,IACL,OAAO,OAAO,SAAS,CAAC;AAAA,IACxB,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,WACd,QACA,SACA,QACkB;AAClB,QAAM,MAAM,GAAG,OAAO,IAAI,MAAM;AAChC,SAAO,OAAO,MAAM,GAAG,KAAK,OAAO,WAAW;AAChD;;;AFhBA,SAAS,cAAc,aAA0B,YAAsC;AACrF,MAAI,CAAC,WAAY;AAEjB,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,CAAC,OAAQ;AAEb,QAAM,OAAO,YAAY,MAAM,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM,EAAE,KAAK,YAAY;AACpF,MAAI,CAAC,KAAM;AAEX,UAAQ,IAAI,aAAa,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM;AAC3D,UAAQ,IAAI,aAAa,KAAK;AAC9B,MAAI,KAAK,UAAU;AACjB,YAAQ,IAAI,iBAAiB,KAAK;AAAA,EACpC;AACF;AAEO,IAAM,SAAS;AAAA,EACpB,IAAI;AAAA,EAEJ,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,YAAY;AAAA,MACV,YAAY,EAAE,MAAM,UAAmB,SAAS,wBAAwB;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,SAAS,KAAgB,SAAuB,CAAC,GAAG;AAClD,UAAM,YAAY,QAAQ,OAAO,cAAc,uBAAuB;AACtE,UAAM,cAAc,UAAU,SAAS;AAEvC,QAAI,GAAG,iBAAiB,CAAC,QAAQ;AAC/B,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAED,QAAI,GAAG,oBAAoB,CAAC,QAAQ;AAClC,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAAA,EACH;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/parse-session-key.ts","../src/load-users.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { parseSessionKey } from './parse-session-key.js';\nimport { loadUsers, type UsersConfig } from './load-users.js';\n\nexport { parseSessionKey, type ParsedSessionKey } from './parse-session-key.js';\nexport { loadUsers, lookupUser, type UserEntry, type UsersConfig } from './load-users.js';\n\ninterface PluginConfig {\n users_file?: string;\n}\n\ninterface PluginApi {\n on(event: string, handler: (ctx: { sessionKey?: string }) => void): void;\n}\n\nfunction applyIdentity(usersConfig: UsersConfig, sessionKey: string | undefined): void {\n if (!sessionKey) return;\n\n const parsed = parseSessionKey(sessionKey);\n if (!parsed) return;\n\n const user = usersConfig.users[`${parsed.channel}:${parsed.peerId}`] ?? usersConfig.default;\n if (!user) return;\n\n process.env.GRWND_USER = `${parsed.channel}:${parsed.peerId}`;\n process.env.GRWND_ROLE = user.role;\n if (user.org_unit) {\n process.env.GRWND_ORG_UNIT = user.org_unit;\n }\n}\n\nexport const plugin = {\n id: 'grwnd-openclaw-governance',\n\n configSchema: {\n type: 'object' as const,\n properties: {\n users_file: { type: 'string' as const, default: './openclaw-users.yaml' },\n },\n },\n\n register(api: PluginApi, config: PluginConfig = {}) {\n const usersFile = resolve(config.users_file ?? './openclaw-users.yaml');\n const usersConfig = loadUsers(usersFile);\n\n api.on('session_start', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n\n api.on('message_received', (ctx) => {\n applyIdentity(usersConfig, ctx.sessionKey);\n });\n },\n};\n","export interface ParsedSessionKey {\n agentId: string;\n channel: string;\n chatType: 'dm' | 'group';\n peerId: string;\n groupId?: string;\n}\n\n/**\n * Parse an OpenClaw session key into its components.\n *\n * Supported formats:\n * agent:<agentId>:<channel>:dm:<peerId>\n * agent:<agentId>:<channel>:group:<groupId>:<peerId>\n *\n * Returns null for unrecognised formats (e.g. \"agent:<id>:main\").\n */\nexport function parseSessionKey(key: string): ParsedSessionKey | null {\n const parts = key.split(':');\n\n if (parts[0] !== 'agent' || parts.length < 5) return null;\n\n const agentId = parts[1]!;\n const channel = parts[2]!;\n const chatType = parts[3];\n\n if (chatType === 'dm' && parts.length === 5) {\n return { agentId, channel, chatType: 'dm', peerId: parts[4]! };\n }\n\n if (chatType === 'group' && parts.length === 6) {\n return { agentId, channel, chatType: 'group', groupId: parts[4]!, peerId: parts[5]! };\n }\n\n return null;\n}\n","import { readFileSync } from 'node:fs';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface UserEntry {\n role: string;\n org_unit?: string;\n}\n\nexport interface UsersConfig {\n users: Record<string, UserEntry>;\n default?: UserEntry;\n}\n\n/** Load and parse an openclaw-users.yaml file. */\nexport function loadUsers(filePath: string): UsersConfig {\n const raw = readFileSync(filePath, 'utf-8');\n const parsed = parseYaml(raw) as UsersConfig;\n return {\n users: parsed.users ?? {},\n default: parsed.default,\n };\n}\n\n/** Look up a channel user, falling back to `default`. Returns null if neither matches. */\nexport function lookupUser(config: UsersConfig, channel: string, peerId: string): UserEntry | null {\n const key = `${channel}:${peerId}`;\n return config.users[key] ?? config.default ?? null;\n}\n"],"mappings":";AAAA,SAAS,eAAe;;;ACiBjB,SAAS,gBAAgB,KAAsC;AACpE,QAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,MAAI,MAAM,CAAC,MAAM,WAAW,MAAM,SAAS,EAAG,QAAO;AAErD,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,UAAU,MAAM,CAAC;AACvB,QAAM,WAAW,MAAM,CAAC;AAExB,MAAI,aAAa,QAAQ,MAAM,WAAW,GAAG;AAC3C,WAAO,EAAE,SAAS,SAAS,UAAU,MAAM,QAAQ,MAAM,CAAC,EAAG;AAAA,EAC/D;AAEA,MAAI,aAAa,WAAW,MAAM,WAAW,GAAG;AAC9C,WAAO,EAAE,SAAS,SAAS,UAAU,SAAS,SAAS,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,EAAG;AAAA,EACtF;AAEA,SAAO;AACT;;;ACnCA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,iBAAiB;AAa5B,SAAS,UAAU,UAA+B;AACvD,QAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO;AAAA,IACL,OAAO,OAAO,SAAS,CAAC;AAAA,IACxB,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,WAAW,QAAqB,SAAiB,QAAkC;AACjG,QAAM,MAAM,GAAG,OAAO,IAAI,MAAM;AAChC,SAAO,OAAO,MAAM,GAAG,KAAK,OAAO,WAAW;AAChD;;;AFZA,SAAS,cAAc,aAA0B,YAAsC;AACrF,MAAI,CAAC,WAAY;AAEjB,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,CAAC,OAAQ;AAEb,QAAM,OAAO,YAAY,MAAM,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM,EAAE,KAAK,YAAY;AACpF,MAAI,CAAC,KAAM;AAEX,UAAQ,IAAI,aAAa,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM;AAC3D,UAAQ,IAAI,aAAa,KAAK;AAC9B,MAAI,KAAK,UAAU;AACjB,YAAQ,IAAI,iBAAiB,KAAK;AAAA,EACpC;AACF;AAEO,IAAM,SAAS;AAAA,EACpB,IAAI;AAAA,EAEJ,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,YAAY;AAAA,MACV,YAAY,EAAE,MAAM,UAAmB,SAAS,wBAAwB;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,SAAS,KAAgB,SAAuB,CAAC,GAAG;AAClD,UAAM,YAAY,QAAQ,OAAO,cAAc,uBAAuB;AACtE,UAAM,cAAc,UAAU,SAAS;AAEvC,QAAI,GAAG,iBAAiB,CAAC,QAAQ;AAC/B,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAED,QAAI,GAAG,oBAAoB,CAAC,QAAQ;AAClC,oBAAc,aAAa,IAAI,UAAU;AAAA,IAC3C,CAAC;AAAA,EACH;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grwnd/openclaw-governance",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "OpenClaw identity bridge plugin for @grwnd/pi-governance",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Grwnd AI",