@rubytech/create-maxy-code 0.1.275 → 0.1.277

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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  3. package/payload/platform/plugins/admin/PLUGIN.md +1 -4
  4. package/payload/platform/plugins/admin/mcp/dist/index.js +0 -8
  5. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  6. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
  7. package/payload/platform/plugins/docs/references/admin-identity-gate.md +77 -81
  8. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  9. package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
  10. package/payload/platform/scripts/setup-account.sh +0 -5
  11. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
  13. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  14. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
  15. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/http-server.js +52 -10
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/index.js +8 -17
  19. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +4 -5
  22. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  23. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +8 -0
  24. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -1
  25. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +13 -0
  26. package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/wa-channel-store.d.ts +29 -0
  28. package/payload/platform/services/claude-session-manager/dist/wa-channel-store.d.ts.map +1 -0
  29. package/payload/platform/services/claude-session-manager/dist/wa-channel-store.js +124 -0
  30. package/payload/platform/services/claude-session-manager/dist/wa-channel-store.js.map +1 -0
  31. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +23 -0
  32. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  33. package/payload/platform/services/whatsapp-channel/dist/notification.js +26 -0
  34. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  35. package/payload/platform/services/whatsapp-channel/dist/server.js +41 -14
  36. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  37. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +1 -1
  38. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  39. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +2 -0
  40. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  41. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +1 -1
  42. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  43. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +1 -1
  44. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +3 -1
  45. package/payload/server/{chunk-W4EM7RK4.js → chunk-QHD5TKLQ.js} +2 -0
  46. package/payload/server/maxy-edge.js +1 -1
  47. package/payload/server/server.js +551 -405
  48. package/payload/platform/plugins/admin/hooks/__tests__/pin-identity-gate.test.sh +0 -96
  49. package/payload/platform/plugins/admin/hooks/pin-identity-gate.sh +0 -146
@@ -1,81 +1,77 @@
1
- # Native-CC admin PIN identity gate
2
-
3
- Every native-CC admin session the operator chatting through claude.ai/code (web,
4
- desktop, mobile) over the per-account `claude rc` daemon must bind a
5
- PIN-authenticated `userId` before any tool runs. This closes the gap where the
6
- daemon stamped one boot-time `USER_ID` (from `users.json`) onto every session,
7
- which is correct for at most one human on a multi-admin install. (Task 619.)
8
-
9
- The legacy cookie/PIN browser login (`POST /api/admin/session`) is a separate
10
- surface documented in [`admin-session.md`](admin-session.md); this gate covers the
11
- native path, which that login never touched.
12
-
13
- ## How it works
14
-
15
- Three pieces, plus a standing audit. No cross-process `session_id` plumbing: the
16
- hook keys state by the `session_id` it reads from stdin; validation and profile
17
- resolution live behind one loopback endpoint.
18
-
19
- 1. **Shared validator** `ui/app/lib/admin-identity/pin-validator.ts`. The single
20
- PIN primitive (`validatePin` `match` / `miss` / `unavailable` / `corrupt`),
21
- shared by the legacy browser login and the gate so the two cannot drift.
22
-
23
- 2. **Loopback validate endpoint** `POST /api/admin/identity/validate`
24
- (`ui/server/routes/admin/identity.ts`). Loopback-only (rejects any request
25
- carrying `x-forwarded-for`, which only edge-proxied browser requests have).
26
- Body `{ pin, sessionId? }`. On a match it resolves the operator's `userName`
27
- and `aboutOwner` via the same helpers the browser path uses
28
- (`resolveUserIdentity`, `resolveOwnerProfileBlock`) `loadUserProfile` is never
29
- duplicated. On a miss with a `sessionId` it fires a best-effort manager
30
- `POST /<sessionId>/stop` (only the UI process reliably holds the manager port).
31
-
32
- 3. **PreToolUse gate hook** — `plugins/admin/hooks/pin-identity-gate.sh`, wired in
33
- `setup-account.sh` as the first universal PreToolUse entry. Runs only for native
34
- operator sessions (`MAXY_SESSION_ROLE=admin` and empty `MAXY_SPECIALIST` the
35
- latter excludes pty-spawned specialist/recorder sessions). Per tool call: if an
36
- auth-state file exists for the `session_id`, allow; if the tool is
37
- `admin-identity-authenticate`, post the PIN to the validate endpoint and on a
38
- match write the state file and allow (on a miss/unavailable, deny + terminate);
39
- any other tool while unauthenticated is denied with an instruction to
40
- authenticate first. Enforcement is in the hook, not the agent's choice — a model
41
- that skips the prompt reaches no other tool.
42
-
43
- The agent-callable surface is the eager-loaded `admin-identity-authenticate` MCP
44
- tool (admin plugin); the gate validates and binds before allowing it, and the tool
45
- returns the authenticated operator's profile to the agent.
46
-
47
- Auth state: `<accountDir>/state/admin-identity/<session_id>.json` (`{userId, ts}`).
48
- One file per authenticated conversation; bound for the session lifetime, not
49
- re-prompted per message. Task-tool specialist subagents share the operator's
50
- `session_id`, so they pass once the operator is authenticated.
51
-
52
- ## Cases
53
-
54
- - **Zero admins / absent / empty `users.json`** → `pin-unavailable`, the gate
55
- refuses; never a bound empty `userId`. A real install always has `users.json`
56
- (the installer/seed writes it), so this is a misconfiguration guard.
57
- - **One admin** still prompts; the single matching PIN binds that `userId`.
58
- - **Many admins** each PIN binds its own `userId`. The account is fixed (the
59
- install's own scope); there is no multi-account picker on this path.
60
- - **Mismatch** the session is terminated on the first miss (no retry budget).
61
-
62
- ## Observability
63
-
64
- One structured `[admin-identity]` line per step, correlated by `sessionId`:
65
-
66
- ```
67
- op=loopback-spawn-received op=pin-prompted → (op=pin-bound source=pin |
68
- op=pin-miss → op=session-terminated) ; the MCP tool emits op=about-owner-loaded.
69
- ```
70
-
71
- **Standing audit** (`services/claude-session-manager/src/admin-identity-audit.ts`,
72
- every 60s) reconciles, independently of the hook, live native-operator sessions
73
- (watcher rows with `role === null` daemon-spawned, no sidecar) that reached real
74
- tool use against the auth-state files. Any such session with no state file emits
75
- `[admin-identity] op=audit-orphan sessionId=…
76
- reason=loopback-session-without-authenticated-userId`. This is the no-event
77
- detector: a silent gate bypass leaves no per-session log line, but the audit still
78
- surfaces it.
79
-
80
- Diagnostic path: `journalctl --user -u <brand>-admin | grep -E '\[admin-identity\]'`,
81
- then filter by `sessionId` for one session's full lifeline.
1
+ # Admin identity model
2
+
3
+ Admin identity is carried by the surface a session arrives on. It is never gated
4
+ by a per-session step, and nothing a non-interactive channel cannot satisfy ever
5
+ blocks a tool.
6
+
7
+ The PIN identity gate (Task 619) a universal PreToolUse hook that denied every
8
+ tool until the operator submitted a PIN — was retired in Task 681. It deadlocked
9
+ non-interactive surfaces: a WhatsApp channel session can never enter a PIN, so the
10
+ channel reply tool was permanently unreachable. The hook, its `admin-identity-authenticate`
11
+ submit tool, the `/api/admin/identity/validate` loopback endpoint, and the standing
12
+ identity audit were all removed. The shared PIN primitive (`pin-validator.ts`) stays
13
+ the legacy browser login (`POST /api/admin/session`) still uses it.
14
+
15
+ ## How identity is carried, per surface
16
+
17
+ **Channels (WhatsApp, future channels).** The channel binding authenticates the
18
+ operator. An allowlisted sender phone in `account.json` `adminPhones` makes the
19
+ inbound session the `admin` role (`access-control.ts`), and the channel-PTY bridge
20
+ keys the admin session per sender (`bridge.ts`). No PIN, no prompt the sender's
21
+ own number is the credential.
22
+
23
+ **Loopback (claude.ai/code).** The operator chats through the per-account `claude rc`
24
+ daemon over loopback. There is no inbound identifier on this surface, so the session
25
+ carries the single boot-time `USER_ID` (the owner, read from `users.json[0]` at
26
+ daemon start). When more than one admin shares the account, the admin agent identifies
27
+ which person it is speaking with conversationally a convenience that loads the right
28
+ profile and history. This is stated in the admin `IDENTITY.md`: identification is
29
+ "purely as a convenience... never a gate and never blocks any tool"; single-admin
30
+ sessions skip it.
31
+
32
+ ## Accepted boundary
33
+
34
+ On a channel (WhatsApp today), the inbound sender phone is bound to a person's
35
+ `userId` via `users.json` (`UserEntry.phone`), confirmed against the account's
36
+ `admins[]`. The channel admin session carries that `userId` graph writes and
37
+ the `${USER_ID}` MCP env attribute to the actual sender (Task 682). An unbound
38
+ sender (the owner's self-phone, or an `adminPhones` entry with no `users.json`
39
+ phone) falls back to the boot-time owner `userId`.
40
+
41
+ A WhatsApp admin whose inbound arrives as an unresolved LID (a cold mapping
42
+ cache) resolves to their own `userId` via a runtime LID cache
43
+ (`whatsapp-lid-map.json`), populated the first time their LID resolves to their
44
+ phone (Task 686). A LID that has never resolved has no cache entry and still
45
+ falls back to the owner `userId` visible as the `op=admin-identity …
46
+ userId=owner-fallback` log line.
47
+
48
+ On loopback (claude.ai/code), multiple admins resolve to the right *profile*
49
+ conversationally but share the owner's `userId` at the env/MCP layer.
50
+ Deterministic per-admin `userId` on loopback is deliberately out of scope:
51
+ making it deterministic again means a per-session identity step, which is exactly
52
+ the deadlock Task 681 removed.
53
+
54
+ ## Boot-time binding drift gate (Task 687)
55
+
56
+ The channel-admin binding is reassembled at read time from three independently
57
+ operator-maintained sources: `adminPhones` (role allowlist), `admins[]`
58
+ (membership), and `users.json` `UserEntry.phone` (the phone→userId binding). If
59
+ they disagree, `resolveAdminUserId` returns null and the session silently runs
60
+ under the owner `userId` — the fallback named in the boundary above, but here as
61
+ a *misconfiguration*, not an intentional unbound sender.
62
+
63
+ On every server start the platform replays the resolution chain for each
64
+ account's `adminPhones` entry and logs one line per entry that would silently
65
+ fall to the owner `userId`:
66
+
67
+ [admin-identity] binding-drift accountId=<id> phone=<p> reason=no-users-binding
68
+ [admin-identity] binding-drift accountId=<id> phone=<p> reason=userid-not-admin userId=<u>
69
+
70
+ - `no-users-binding` — no `users.json` entry's `phone` matches this admin phone,
71
+ so it can never resolve to a userId. Add the phone to the person's `users.json`
72
+ entry.
73
+ - `userid-not-admin` the phone is bound to `userId`, but that userId is not in
74
+ this account's `admins[]`. Add the userId to `admins[]`.
75
+
76
+ The gate is informational: it never blocks boot, and steady-state boots with
77
+ consistent bindings emit nothing.
@@ -70,7 +70,7 @@ Task 500 — **admin session management moved entirely to claude's own interface
70
70
 
71
71
  | Mount | Purpose |
72
72
  |---|---|
73
- | `/sidebar-artefacts` | Lists every editable artefact for the sidebar Artefacts view (KnowledgeDocuments + this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates). |
73
+ | `/sidebar-artefacts` | Lists the sidebar Artefacts rows: every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'` — all file types, excluding the separate `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates (Task 684). |
74
74
  | `/sidebar-artefact-content` | Reads a single artefact's bytes for the artefact pane. |
75
75
  | `/sidebar-artefact-save` | Persists an artefact edit. |
76
76
  | `/attachment` | Per-attachment binary fetch (images, PDFs, etc.). |
@@ -217,7 +217,7 @@ authoritative. This section names the surfaces and what backs each.
217
217
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
218
218
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. | `/claude-sessions/events`, `/claude-sessions` |
219
219
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
220
- | Artefacts list | Lists every KnowledgeDocument plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | `sidebar-artefacts.ts`, `files.ts` |
220
+ | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree — Task 684) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | `sidebar-artefacts.ts`, `files.ts` |
221
221
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
222
222
  | Footer | Operator avatar, name, role, and the actions popover. | `session.ts` |
223
223
 
@@ -41,6 +41,9 @@ const ALLOWLIST = new Set([
41
41
  // Same vi.hoisted pattern: Task 549's sidebar-sessions test pre-builds a
42
42
  // tmpdir before the module-under-test's ACCOUNTS_DIR mock is wired.
43
43
  'platform/ui/server/routes/admin/__tests__/sidebar-sessions.test.ts',
44
+ // Same vi.hoisted pattern: Task 684's sidebar-artefacts test pre-builds an
45
+ // empty tmpdir for ACCOUNTS_DIR/PLATFORM_ROOT before the module's mocks fire.
46
+ 'platform/ui/server/routes/admin/__tests__/sidebar-artefacts.test.ts',
44
47
  // Same vi.hoisted pattern: Task 576's claude-auth refresh-lock test
45
48
  // builds a tmpdir + creds path before the module's `../paths` mock fires.
46
49
  'platform/ui/app/lib/__tests__/claude-auth-refresh-lock.test.ts',
@@ -79,11 +79,6 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
79
79
  },
80
80
  "hooks": {
81
81
  "PreToolUse": [
82
- {
83
- "hooks": [
84
- { "type": "command", "command": "bash $HOOKS_PATH/pin-identity-gate.sh" }
85
- ]
86
- },
87
82
  {
88
83
  "matcher": "Write",
89
84
  "hooks": [
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAiB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAiLtD,CAAA"}
1
+ {"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAiB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAgLtD,CAAA"}
@@ -33,7 +33,6 @@ export const CANONICAL_MAXY_TOOL_NAMES = [
33
33
  "mcp__plugin_admin_admin__action-pending",
34
34
  "mcp__plugin_admin_admin__action-reject",
35
35
  "mcp__plugin_admin_admin__admin-add",
36
- "mcp__plugin_admin_admin__admin-identity-authenticate",
37
36
  "mcp__plugin_admin_admin__admin-list",
38
37
  "mcp__plugin_admin_admin__admin-remove",
39
38
  "mcp__plugin_admin_admin__admin-update-pin",
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-tool-names.generated.js","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"canonical-tool-names.generated.js","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ import type { FsWatcher } from './fs-watcher.js';
4
4
  import type { RateLimiter } from './spawn-rate-limiter.js';
5
5
  import type { AuditRegistry } from './public-tool-audit.js';
6
6
  import { type UserTitleStore } from './user-title-store.js';
7
+ import { WaChannelStore } from './wa-channel-store.js';
7
8
  export declare const SESSION_ID_PATTERN: RegExp;
8
9
  export declare const RC_SPAWN_URL_WAIT_DEFAULT_MS = 60000;
9
10
  export interface HttpDeps extends Omit<SpawnDeps, 'onSessionReady' | 'watcher'> {
@@ -19,6 +20,10 @@ export interface HttpDeps extends Omit<SpawnDeps, 'onSessionReady' | 'watcher'>
19
20
  /** Task 253 — operator-stamped title override store. Reads merge into
20
21
  * every displayName resolution site; the /:sessionId/rename route writes. */
21
22
  userTitleStore: UserTitleStore;
23
+ /** Task 683 — per-session WhatsApp channel binding. Gateway `/rc-spawn`
24
+ * write-throughs it; every resume path reads it back to re-apply the
25
+ * channel argv so `reply` is present on every live PTY for the session. */
26
+ waChannelStore: WaChannelStore;
22
27
  /** Task 648 — override the `/rc-spawn` pid/bind poll bound. Production
23
28
  * omits this and gets RC_SPAWN_URL_WAIT_DEFAULT_MS (60s); tests inject a
24
29
  * short value to exercise the bind-timeout response without a 60s wait. */
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,OAAO,EAeL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAkBzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAgF9E,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AA+MD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAmBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAyrDpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,OAAO,EAeL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAkBzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAgFtD,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AA+MD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAmBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAsuDpD"}
@@ -23,7 +23,7 @@ import { Hono } from 'hono';
23
23
  import { stream } from 'hono/streaming';
24
24
  import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, mkdirSync, renameSync, readFileSync, writeFileSync } from 'node:fs';
25
25
  import { tmpdir } from 'node:os';
26
- import { buildWaChannelMcpServers, waChannelDevChannelsArgv, isDevChannelPrompt } from './wa-channel-mcp.js';
26
+ import { buildWaChannelMcpServers, buildWaChannelArgv, waChannelMcpConfigPath, isDevChannelPrompt } from './wa-channel-mcp.js';
27
27
  import { randomUUID } from 'node:crypto';
28
28
  import { execFileSync } from 'node:child_process';
29
29
  import { buildRcChildEnv } from './rc-daemon.js';
@@ -1295,6 +1295,10 @@ export function buildHttpApp(deps) {
1295
1295
  // logs its own failure and reads still serve from the in-memory map
1296
1296
  // until next boot.
1297
1297
  deps.userTitleStore.remove(sessionId);
1298
+ // Task 683 — drop the channel binding too so wa-channel-bindings.json
1299
+ // doesn't accrete orphans and a recreated session can't inherit a stale
1300
+ // binding. Best-effort; the store logs its own failure.
1301
+ deps.waChannelStore.remove(sessionId);
1298
1302
  deps.logger(`purge sessionId=${sessionId.slice(0, 8)} bytes=${bytes}`);
1299
1303
  timed(deps.logger, 'DELETE', `/${id}`, 204, Date.now() - start);
1300
1304
  return c.body(null, 204);
@@ -1549,6 +1553,13 @@ export function buildHttpApp(deps) {
1549
1553
  const personId = typeof body.personId === 'string' && body.personId.length > 0 && body.personId.length <= 128
1550
1554
  ? body.personId
1551
1555
  : undefined;
1556
+ // Task 682 — per-session userId override. Server-resolved by the UI from the
1557
+ // authoritative users.json/account.json (never message content) and shape-
1558
+ // validated here the same way as personId. Absent for owner self-phone and
1559
+ // any unbound sender, where the boot-time deps.userId (owner) is used.
1560
+ const overrideUserId = typeof body.userId === 'string' && body.userId.length > 0 && body.userId.length <= 128
1561
+ ? body.userId
1562
+ : undefined;
1552
1563
  // Task 626 — one-shot admin jobs (LinkedIn ingest, public session-end
1553
1564
  // review) carry their prompt as the session's first turn, the same
1554
1565
  // trailing-positional mechanism /public-spawn uses, and close after that
@@ -1595,20 +1606,41 @@ export function buildHttpApp(deps) {
1595
1606
  // development channel. Inbound WhatsApp then arrives as a `<channel
1596
1607
  // source="whatsapp">` event and the agent replies via the channel `reply`
1597
1608
  // tool — no keystroke injection. Pushed before the trailing positional.
1598
- if (waChannel && sessionId) {
1609
+ // Task 683 resolve the effective binding. The gateway `/rc-spawn` supplies
1610
+ // waChannel and we write it through to the store; any later resume (operator
1611
+ // / PIN / manager) omits it, so we read the binding back. Either way the same
1612
+ // per-session --mcp-config + development-channels argv is injected, so
1613
+ // `mcp__whatsapp-channel__reply` is present on every live PTY for the session.
1614
+ let effectiveWaChannel;
1615
+ if (sessionId) {
1616
+ if (waChannel) {
1617
+ effectiveWaChannel = waChannel;
1618
+ deps.waChannelStore.set(sessionId, waChannel);
1619
+ }
1620
+ else {
1621
+ const stored = deps.waChannelStore.get(sessionId);
1622
+ if (stored) {
1623
+ effectiveWaChannel = { senderId: stored.senderId, gatewayUrl: stored.gatewayUrl, serverPath: stored.serverPath };
1624
+ deps.logger(`[rc-spawn] op=wa-channel-rebind sessionId=${sessionId.slice(0, 8)} senderId=${stored.senderId} source=store`);
1625
+ }
1626
+ }
1627
+ }
1628
+ let waPath = null;
1629
+ if (effectiveWaChannel && sessionId) {
1599
1630
  const waServers = buildWaChannelMcpServers({
1600
1631
  sessionId,
1601
- senderId: waChannel.senderId,
1602
- gatewayUrl: waChannel.gatewayUrl,
1603
- serverPath: waChannel.serverPath,
1632
+ senderId: effectiveWaChannel.senderId,
1633
+ gatewayUrl: effectiveWaChannel.gatewayUrl,
1634
+ serverPath: effectiveWaChannel.serverPath,
1604
1635
  });
1605
- const waPath = join(tmpdir(), `maxy-wa-channel-${sessionId.replace(/[^A-Za-z0-9_-]/g, '_')}.json`);
1636
+ waPath = waChannelMcpConfigPath(sessionId, tmpdir());
1606
1637
  try {
1607
1638
  writeFileSync(waPath, JSON.stringify({ mcpServers: waServers }, null, 2), 'utf8');
1608
- argv.push('--mcp-config', waPath, ...waChannelDevChannelsArgv());
1609
- deps.logger(`[rc-spawn] op=wa-channel senderId=${waChannel.senderId} gateway=${waChannel.gatewayUrl} path=${waPath}`);
1639
+ argv.push(...buildWaChannelArgv(waPath));
1640
+ deps.logger(`[rc-spawn] op=wa-channel senderId=${effectiveWaChannel.senderId} gateway=${effectiveWaChannel.gatewayUrl} path=${waPath}`);
1610
1641
  }
1611
1642
  catch (err) {
1643
+ waPath = null;
1612
1644
  deps.logger(`[rc-spawn] op=wa-channel-write-failed err=${JSON.stringify(err instanceof Error ? err.message : String(err))}`);
1613
1645
  }
1614
1646
  }
@@ -1622,7 +1654,7 @@ export function buildHttpApp(deps) {
1622
1654
  // so the child's MCP stderr-tee writes its readable per-server log.
1623
1655
  const env = buildRcChildEnv({
1624
1656
  accountId: deps.accountId,
1625
- userId: deps.userId,
1657
+ userId: overrideUserId ?? deps.userId,
1626
1658
  sessionId: sessionId ?? undefined,
1627
1659
  uiPort: process.env.MAXY_UI_INTERNAL_PORT ?? '',
1628
1660
  platformRoot: deps.platformRoot,
@@ -1690,6 +1722,16 @@ export function buildHttpApp(deps) {
1690
1722
  pid: pty.pid,
1691
1723
  managerOpenFds: openFdCount(),
1692
1724
  });
1725
+ // Task 683 — standing invariant. A bound session whose live PTY was spawned
1726
+ // without the channel mcp-config is the silent-revert state (the reply tool
1727
+ // is absent, the agent falls back to whatsapp-send). The resume read-back
1728
+ // makes the resume-without-binding case impossible, so this now fires only
1729
+ // when the mcp-config write itself failed (waPath reset to null) — it
1730
+ // verifies the post-condition (PTY actually carries the channel) rather than
1731
+ // the intent to spawn it.
1732
+ if (sessionId && deps.waChannelStore.get(sessionId) && waPath === null) {
1733
+ deps.logger(`[rc-spawn] op=channel-binding-lost sessionId=${sessionId} pid=${pty.pid} reason=resume-without-mcp-config`);
1734
+ }
1693
1735
  // Tracker registration — closes the leak gap the Task 552 path opened.
1694
1736
  // For fresh spawns without a caller-supplied sessionId we register the
1695
1737
  // tracker under the unitToken so the entry has a stable key; the
@@ -1751,7 +1793,7 @@ export function buildHttpApp(deps) {
1751
1793
  const BIND_BUF_TAIL = 8 * 1024;
1752
1794
  pty.onData((data) => {
1753
1795
  stdoutBytes += data.length;
1754
- if (waChannel && !devChannelConfirmed) {
1796
+ if (effectiveWaChannel && !devChannelConfirmed) {
1755
1797
  devChannelScan += data;
1756
1798
  if (devChannelScan.length > 4096)
1757
1799
  devChannelScan = devChannelScan.slice(devChannelScan.length - 4096);