@rubytech/create-maxy-code 0.1.573 → 0.1.574
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/package.json +1 -1
- package/payload/platform/lib/admins-write/__tests__/index.test.ts +97 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts +12 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +27 -5
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +35 -5
- package/payload/platform/lib/admins-write/vitest.config.ts +14 -0
- package/payload/platform/lib/dispatch-read/dist/session.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/session.js +19 -4
- package/payload/platform/lib/dispatch-read/dist/session.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/session.test.ts +91 -0
- package/payload/platform/lib/dispatch-read/src/session.ts +25 -6
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +8 -3
- package/payload/platform/plugins/admin/hooks/__tests__/html-structure-gate.test.sh +264 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +122 -24
- package/payload/platform/plugins/admin/hooks/__tests__/specialist-dispatch-gate.test.sh +158 -0
- package/payload/platform/plugins/admin/hooks/html-structure-gate.sh +165 -0
- package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +111 -19
- package/payload/platform/plugins/admin/hooks/specialist-dispatch-gate.sh +159 -0
- package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.d.ts +27 -0
- package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.js +29 -0
- package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/binding-validate.d.ts +11 -0
- package/payload/platform/plugins/admin/mcp/dist/binding-validate.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/binding-validate.js +20 -0
- package/payload/platform/plugins/admin/mcp/dist/binding-validate.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +133 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +32 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +157 -46
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +71 -31
- package/payload/platform/plugins/docs/references/channel-wake-and-prompt.md +38 -13
- package/payload/platform/plugins/docs/references/internals.md +6 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +31 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +10 -0
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.js +177 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.js +130 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.js +162 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.js +168 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.d.ts +64 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.js +110 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.d.ts +119 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.js +0 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.js +247 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js +10 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +21 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts +16 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js +5 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js +2 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +24 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +2 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +29 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.js +21 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.d.ts +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.js +10 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +54 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.d.ts +32 -0
- package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.js +144 -0
- package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/html-structure-census.d.ts +44 -0
- package/payload/platform/services/claude-session-manager/dist/html-structure-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/html-structure-census.js +185 -0
- package/payload/platform/services/claude-session-manager/dist/html-structure-census.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.js +125 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/quote-print-census.d.ts +42 -0
- package/payload/platform/services/claude-session-manager/dist/quote-print-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/quote-print-census.js +254 -0
- package/payload/platform/services/claude-session-manager/dist/quote-print-census.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-census.d.ts +92 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-census.js +227 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-census.js.map +1 -0
- package/payload/platform/templates/account-settings.json +8 -4
- package/payload/server/server.js +465 -361
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# specialist-dispatch-gate — PreToolUse gate on every tool.
|
|
3
|
+
#
|
|
4
|
+
# An account's own specialists own their tools. This gate refuses the main
|
|
5
|
+
# conversation a tool one of the account's authored cards declares, and names the
|
|
6
|
+
# specialist to dispatch instead. It exists because skipped delegation writes no
|
|
7
|
+
# record: an inline draft logs an ordinary success, so nothing in any log
|
|
8
|
+
# separates "the clerk did it" from "the admin did it by hand". On one account
|
|
9
|
+
# four authored clerks owned email, works orders, invoices and quotes while the
|
|
10
|
+
# admin produced all of them inline for three days.
|
|
11
|
+
#
|
|
12
|
+
# Two narrowings, both load-bearing.
|
|
13
|
+
#
|
|
14
|
+
# Card axis — the account's OWN cards only. Provisioning copies every shipped
|
|
15
|
+
# template into each account, and one of them declares roughly 130 tools
|
|
16
|
+
# including the whole email, WhatsApp, calendar and browser surfaces, so reading
|
|
17
|
+
# every card would refuse the admin nearly its entire seat. A card is the
|
|
18
|
+
# account's own when its filename appears in none of the shipped roots. The
|
|
19
|
+
# account's own plugin root is deliberately NOT one of those: a card filed there
|
|
20
|
+
# is that account's durable copy, not something we ship.
|
|
21
|
+
#
|
|
22
|
+
# Tool axis — native tools are never gated. Every card declares some of Bash,
|
|
23
|
+
# Read, Write and Edit, and refusing those would brick the seat. The NATIVE set
|
|
24
|
+
# below mirrors CC_NATIVE_TOOLS in the session manager, which a bash hook cannot
|
|
25
|
+
# import; the gate's own test compares the two sets rather than trusting them to
|
|
26
|
+
# stay equal.
|
|
27
|
+
#
|
|
28
|
+
# Caller identity is `agent_type` from the hook envelope, which Claude Code sets
|
|
29
|
+
# when a hook fires inside a subagent. A non-empty value exits 0 unconditionally:
|
|
30
|
+
# this gate must never filter a subagent's tool surface, which is the failure an
|
|
31
|
+
# inherited CLI deny produced when it stripped the writers from a specialist's
|
|
32
|
+
# own frontmatter. The MAXY_SPECIALIST env is not usable — it is stamped only for
|
|
33
|
+
# a specialist spawned as its own PTY process, and native Agent-tool subagents
|
|
34
|
+
# inherit the admin process env.
|
|
35
|
+
#
|
|
36
|
+
# Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail OPEN on an
|
|
37
|
+
# uninspectable envelope, and say so with op=degraded when the card set cannot be
|
|
38
|
+
# read: failing open is fine, failing open silently is the fault. No task numbers
|
|
39
|
+
# or internal refs in any operator-visible string.
|
|
40
|
+
|
|
41
|
+
set -uo pipefail
|
|
42
|
+
|
|
43
|
+
# No envelope (tty or empty stdin) → allow: cannot inspect, must not brick tools.
|
|
44
|
+
if [ -t 0 ]; then exit 0; fi
|
|
45
|
+
INPUT=$(cat)
|
|
46
|
+
[ -z "$INPUT" ] && exit 0
|
|
47
|
+
|
|
48
|
+
# Prints nothing for an out-of-scope call, "block <specialist>" for a refusal, or
|
|
49
|
+
# "degraded <reason>" when the card set could not be read.
|
|
50
|
+
DECISION=$(INPUT="$INPUT" python3 - <<'PY' 2>/dev/null || true
|
|
51
|
+
import os, json, re, sys, glob
|
|
52
|
+
|
|
53
|
+
# Mirrors CC_NATIVE_TOOLS (claude-session-manager/src/specialist-drift.ts). A
|
|
54
|
+
# card that declares these declares them for its own use; they are the admin's
|
|
55
|
+
# too, and gating them would refuse the seat its own disk and shell.
|
|
56
|
+
NATIVE = {
|
|
57
|
+
"Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "LS", "Bash",
|
|
58
|
+
"BashOutput", "KillShell", "Skill", "WebSearch", "WebFetch", "Task",
|
|
59
|
+
"TodoWrite", "NotebookEdit", "NotebookRead", "AskUserQuestion",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
d = json.loads(os.environ["INPUT"])
|
|
64
|
+
except Exception:
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
tool = (d.get("tool_name", "") or "").strip()
|
|
68
|
+
if not tool or tool in NATIVE:
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
# A subagent is never filtered. This is the whole reason the gate can be broad:
|
|
72
|
+
# it constrains one seat, not a tool surface.
|
|
73
|
+
if (d.get("agent_type") or "").strip():
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
agents_dir = os.path.join(os.getcwd(), "specialists", "agents")
|
|
77
|
+
platform_root = os.environ.get("PLATFORM_ROOT", "")
|
|
78
|
+
|
|
79
|
+
shipped = set()
|
|
80
|
+
try:
|
|
81
|
+
if platform_root:
|
|
82
|
+
for f in glob.glob(os.path.join(platform_root, "templates", "specialists", "agents", "*.md")):
|
|
83
|
+
shipped.add(os.path.basename(f))
|
|
84
|
+
for f in glob.glob(os.path.join(platform_root, "plugins", "*", "agents", "*.md")):
|
|
85
|
+
shipped.add(os.path.basename(f))
|
|
86
|
+
premium = os.path.join(os.path.dirname(platform_root), "premium-plugins")
|
|
87
|
+
for f in glob.glob(os.path.join(premium, "*", "agents", "*.md")):
|
|
88
|
+
shipped.add(os.path.basename(f))
|
|
89
|
+
if not os.path.isdir(agents_dir):
|
|
90
|
+
# An account with no specialists directory is normal, not degraded.
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
cards = sorted(os.listdir(agents_dir))
|
|
93
|
+
except Exception as err:
|
|
94
|
+
print("degraded %s" % type(err).__name__)
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fields(path):
|
|
99
|
+
"""(name, tools) from a card's frontmatter, or None when there is none."""
|
|
100
|
+
try:
|
|
101
|
+
with open(path, encoding="utf-8") as fh:
|
|
102
|
+
lines = fh.read().split("\n")
|
|
103
|
+
except Exception:
|
|
104
|
+
return None
|
|
105
|
+
if not lines or lines[0].strip() != "---":
|
|
106
|
+
return None
|
|
107
|
+
name = tools = None
|
|
108
|
+
for line in lines[1:]:
|
|
109
|
+
if line.strip() == "---":
|
|
110
|
+
break
|
|
111
|
+
m = re.match(r'^name:\s*(.+?)\s*$', line)
|
|
112
|
+
if m:
|
|
113
|
+
name = m.group(1).strip().strip('"').strip("'")
|
|
114
|
+
m = re.match(r'^tools:\s*(.+?)\s*$', line)
|
|
115
|
+
if m:
|
|
116
|
+
tools = m.group(1)
|
|
117
|
+
return (name, tools)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
for filename in cards:
|
|
121
|
+
if not filename.endswith(".md"):
|
|
122
|
+
continue
|
|
123
|
+
if filename in shipped:
|
|
124
|
+
continue
|
|
125
|
+
got = fields(os.path.join(agents_dir, filename))
|
|
126
|
+
if not got:
|
|
127
|
+
continue
|
|
128
|
+
name, tools = got
|
|
129
|
+
if not name or not tools:
|
|
130
|
+
continue
|
|
131
|
+
# Match the tool itself, never a substring of a longer name.
|
|
132
|
+
for declared in tools.split(","):
|
|
133
|
+
declared = declared.strip()
|
|
134
|
+
if not declared or declared in NATIVE:
|
|
135
|
+
continue
|
|
136
|
+
if declared == tool:
|
|
137
|
+
print("block %s" % name)
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
PY
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
[ -z "$DECISION" ] && exit 0
|
|
143
|
+
|
|
144
|
+
VERB=${DECISION%% *}
|
|
145
|
+
ARG=${DECISION#* }
|
|
146
|
+
|
|
147
|
+
if [ "$VERB" = "degraded" ]; then
|
|
148
|
+
echo "[specialist-gate] op=degraded reason=$ARG" >&2
|
|
149
|
+
exit 0
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
ACCOUNT_ID8=$(basename "$PWD" | cut -c1-8)
|
|
153
|
+
TOOL=$(printf '%s' "$INPUT" \
|
|
154
|
+
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("tool_name",""))' 2>/dev/null \
|
|
155
|
+
|| echo "")
|
|
156
|
+
|
|
157
|
+
echo "[specialist-gate] op=refuse account=$ACCOUNT_ID8 tool=$TOOL specialist=$ARG" >&2
|
|
158
|
+
echo "Blocked: this tool belongs to your $ARG. Dispatch that specialist with the Agent tool and let it do the work — it holds the tool and the standards that go with it." >&2
|
|
159
|
+
exit 2
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type AdminBindVerdict = {
|
|
2
|
+
kind: "ok";
|
|
3
|
+
pin: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: "nothing-to-set";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "bad-phone";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "bad-telegram";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "no-such-user";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "not-an-admin";
|
|
14
|
+
};
|
|
15
|
+
export declare function classifyAdminBind(input: {
|
|
16
|
+
userId: string;
|
|
17
|
+
phone?: string;
|
|
18
|
+
telegram?: string;
|
|
19
|
+
users: ReadonlyArray<{
|
|
20
|
+
userId: string;
|
|
21
|
+
pin: string;
|
|
22
|
+
}>;
|
|
23
|
+
admins: ReadonlyArray<{
|
|
24
|
+
userId: string;
|
|
25
|
+
}>;
|
|
26
|
+
}): AdminBindVerdict;
|
|
27
|
+
//# sourceMappingURL=admin-bind-classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-bind-classify.d.ts","sourceRoot":"","sources":["../src/admin-bind-classify.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC;AAE7B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,MAAM,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3C,GAAG,gBAAgB,CAUnB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 2531 — the pure refusal policy for `admin-bind`, split from the IO
|
|
3
|
+
* wrapper for the same reason `resolve-admin-userid.ts` splits its picker: every
|
|
4
|
+
* refusal is then unit-testable without a filesystem, and the policy cannot
|
|
5
|
+
* drift from the tool that enforces it.
|
|
6
|
+
*
|
|
7
|
+
* Never infers a userId. `addAdminPhone` takes an account directory and a phone
|
|
8
|
+
* with no userId, which is exactly why it cannot write a binding: the allowlist
|
|
9
|
+
* knows a number, not a person. Binding runs the other way, so the caller names
|
|
10
|
+
* whom and an unknown userId is refused rather than guessed at.
|
|
11
|
+
*/
|
|
12
|
+
import { isE164, isTelegramChatId } from "./binding-validate.js";
|
|
13
|
+
export function classifyAdminBind(input) {
|
|
14
|
+
if (input.phone === undefined && input.telegram === undefined)
|
|
15
|
+
return { kind: "nothing-to-set" };
|
|
16
|
+
if (input.phone !== undefined && !isE164(input.phone))
|
|
17
|
+
return { kind: "bad-phone" };
|
|
18
|
+
if (input.telegram !== undefined && !isTelegramChatId(input.telegram))
|
|
19
|
+
return { kind: "bad-telegram" };
|
|
20
|
+
const row = input.users.find(u => u.userId === input.userId);
|
|
21
|
+
if (!row)
|
|
22
|
+
return { kind: "no-such-user" };
|
|
23
|
+
if (!input.admins.some(a => a.userId === input.userId))
|
|
24
|
+
return { kind: "not-an-admin" };
|
|
25
|
+
// The existing hash, returned so the caller can route through writeAdminEntry
|
|
26
|
+
// (which requires a pin) without rotating the credential it is not touching.
|
|
27
|
+
return { kind: "ok", pin: row.pin };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=admin-bind-classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-bind-classify.js","sourceRoot":"","sources":["../src/admin-bind-classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAUjE,MAAM,UAAU,iBAAiB,CAAC,KAMjC;IACC,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACjG,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACpF,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IACvG,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7D,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IACxF,8EAA8E;IAC9E,6EAA6E;IAC7E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 2531 — the two channel-binding shapes, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Imported by `index.ts` (admin-add's own validation) and by
|
|
5
|
+
* `admin-bind-classify.ts` (admin-bind's refusal policy), so the tool and the
|
|
6
|
+
* policy cannot disagree about what a valid binding is. Two copies of a
|
|
7
|
+
* validation rule drift the same way two copies of the record type did.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isE164(v: string): boolean;
|
|
10
|
+
export declare function isTelegramChatId(v: string): boolean;
|
|
11
|
+
//# sourceMappingURL=binding-validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding-validate.d.ts","sourceRoot":"","sources":["../src/binding-validate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzC;AAMD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEnD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 2531 — the two channel-binding shapes, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Imported by `index.ts` (admin-add's own validation) and by
|
|
5
|
+
* `admin-bind-classify.ts` (admin-bind's refusal policy), so the tool and the
|
|
6
|
+
* policy cannot disagree about what a valid binding is. Two copies of a
|
|
7
|
+
* validation rule drift the same way two copies of the record type did.
|
|
8
|
+
*/
|
|
9
|
+
/** E.164, the same shape `config-persist.ts` enforces for `adminPhones`. */
|
|
10
|
+
const E164 = /^\+[1-9]\d{1,14}$/;
|
|
11
|
+
export function isE164(v) {
|
|
12
|
+
return E164.test(v);
|
|
13
|
+
}
|
|
14
|
+
/** A Telegram chat id is an integer. A group chat's is negative, so the leading
|
|
15
|
+
* minus is legal and refusing it would reject a real binding. */
|
|
16
|
+
const TG_CHAT_ID = /^-?\d+$/;
|
|
17
|
+
export function isTelegramChatId(v) {
|
|
18
|
+
return TG_CHAT_ID.test(v);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=binding-validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding-validate.js","sourceRoot":"","sources":["../src/binding-validate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,4EAA4E;AAC5E,MAAM,IAAI,GAAG,mBAAmB,CAAC;AAEjC,MAAM,UAAU,MAAM,CAAC,CAAS;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACtB,CAAC;AAED;kEACkE;AAClE,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,MAAM,UAAU,gBAAgB,CAAC,CAAS;IACxC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -9,6 +9,8 @@ import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSyn
|
|
|
9
9
|
import { writeAdminEntry, removeAdminFromAccount, logUsersAudit, formatAuditRowIds } from "../../../../lib/admins-write/dist/index.js";
|
|
10
10
|
import { setAccessPassword as setAccessPasswordHash, clearAccessPassword as clearAccessPasswordHash, accessPasswordCollides } from "../../../../lib/admin-access-password/dist/index.js";
|
|
11
11
|
import { resolveCredentialTarget } from "./credential-identity.js";
|
|
12
|
+
import { isE164, isTelegramChatId } from "./binding-validate.js";
|
|
13
|
+
import { classifyAdminBind } from "./admin-bind-classify.js";
|
|
12
14
|
import { embed } from "../../../../lib/embed-client/dist/index.js";
|
|
13
15
|
import { requirePortEnv } from "../../../../lib/require-port-env/dist/index.js";
|
|
14
16
|
import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
|
|
@@ -286,6 +288,32 @@ function readUsersJson() {
|
|
|
286
288
|
function writeUsersJson(users) {
|
|
287
289
|
writeFileSync(USERS_FILE, JSON.stringify(users, null, 2) + "\n", "utf-8");
|
|
288
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Task 2531 — re-read users.json after a binding write and report what is
|
|
293
|
+
* actually on disk.
|
|
294
|
+
*
|
|
295
|
+
* "Called the writer" is not evidence the field landed. The whole reason a
|
|
296
|
+
* missing binding went unnoticed for the life of this install is that nothing
|
|
297
|
+
* ever checked the post-condition; the boot-time drift check reports it, but
|
|
298
|
+
* only on the NEXT boot. This closes that gap at the moment of the write.
|
|
299
|
+
* Returns the observed verdict and logs it either way.
|
|
300
|
+
*/
|
|
301
|
+
function verifyBindingWrite(tag, userId, want) {
|
|
302
|
+
let ok = false;
|
|
303
|
+
try {
|
|
304
|
+
const row = readUsersJson().find(u => u.userId === userId);
|
|
305
|
+
ok =
|
|
306
|
+
!!row &&
|
|
307
|
+
(want.phone === undefined || row.phone === want.phone) &&
|
|
308
|
+
(want.telegram === undefined || row.telegram === want.telegram);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
ok = false;
|
|
312
|
+
}
|
|
313
|
+
const state = (v) => (v === undefined ? "absent" : "set");
|
|
314
|
+
console.error(`${tag} op=binding-write userId=${userId.slice(0, 8)} phone=${state(want.phone)} telegram=${state(want.telegram)} verified=${ok}`);
|
|
315
|
+
return ok;
|
|
316
|
+
}
|
|
289
317
|
function generateUniquePin(users, maxRetries = 10) {
|
|
290
318
|
const existingPins = new Set(users.map(u => u.pin));
|
|
291
319
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -965,7 +993,9 @@ lifelineTool(server, "plugin-toggle-enabled", "Enable or disable a plugin in thi
|
|
|
965
993
|
eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a device-level user entry (users.json) and adds them to this account's admins list (account.json). PIN must be at least 4 digits. If no PIN is provided, a unique 4-digit PIN is generated. Returns the userId and PIN to share with the new admin.\n\nIMPORTANT — retry behaviour: if the user already stated a specific PIN earlier in the conversation and a first admin-add call failed (e.g. tier cap reached, PIN collision), every retry MUST re-pass that PIN as the `pin` parameter. Omitting `pin` on the retry auto-generates a different 4-digit PIN, silently substituting what the user asked for.", {
|
|
966
994
|
name: z.string().describe("Display name for the new admin (stored on the AdminUser node in Neo4j)."),
|
|
967
995
|
pin: z.string().optional().describe("Optional PIN (minimum 4 digits). If omitted, a unique 4-digit PIN is generated."),
|
|
968
|
-
|
|
996
|
+
phone: z.string().optional().describe("E.164 phone (e.g. +441234567890) binding this admin to their WhatsApp sender. Without it a WhatsApp turn from this person resolves to no admin and their answer reaches only the door it arrived on. Settable later with admin-bind."),
|
|
997
|
+
telegram: z.string().optional().describe("Telegram chat id binding this admin to their Telegram chat (negative for a group). Without it a Telegram turn from this person resolves to no admin. Settable later with admin-bind."),
|
|
998
|
+
}, async ({ name, pin: rawPin, phone, telegram }) => {
|
|
969
999
|
if (!ACCOUNT_ID)
|
|
970
1000
|
return refuseNoAccount("admin-add");
|
|
971
1001
|
const TAG = "[admin]";
|
|
@@ -984,6 +1014,16 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
|
|
|
984
1014
|
process.stderr.write(`[admin-add] op=refused reason=no-name\n`);
|
|
985
1015
|
return { content: [{ type: "text", text: `${TAG} A real name is required (it cannot be blank or the placeholder "Owner").` }], isError: true };
|
|
986
1016
|
}
|
|
1017
|
+
// Task 2531 — validate the bindings before any store write, so a malformed
|
|
1018
|
+
// one cannot land a half-created admin.
|
|
1019
|
+
if (phone !== undefined && !isE164(phone)) {
|
|
1020
|
+
process.stderr.write(`[admin-add] op=refused reason=bad-phone\n`);
|
|
1021
|
+
return { content: [{ type: "text", text: `${TAG} phone must be E.164, e.g. +441234567890.` }], isError: true };
|
|
1022
|
+
}
|
|
1023
|
+
if (telegram !== undefined && !isTelegramChatId(telegram)) {
|
|
1024
|
+
process.stderr.write(`[admin-add] op=refused reason=bad-telegram\n`);
|
|
1025
|
+
return { content: [{ type: "text", text: `${TAG} telegram must be a chat id (digits, optionally negative for a group).` }], isError: true };
|
|
1026
|
+
}
|
|
987
1027
|
let users;
|
|
988
1028
|
try {
|
|
989
1029
|
users = readUsersJson();
|
|
@@ -1053,11 +1093,20 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
|
|
|
1053
1093
|
accountDir: getAccountDir(),
|
|
1054
1094
|
caller: "admin-add",
|
|
1055
1095
|
audit: { actor: callerUserId ?? "unknown", session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG },
|
|
1096
|
+
// Task 2531 — the channel bindings, written by the same chokepoint as the
|
|
1097
|
+
// PIN so a new admin can be born reachable on their own channels.
|
|
1098
|
+
phone,
|
|
1099
|
+
telegram,
|
|
1056
1100
|
});
|
|
1057
1101
|
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=${authResult.usersJsonResult} store=users${authResult.usersError ? ` error=${authResult.usersError}` : ""}`);
|
|
1058
1102
|
if (authResult.usersJsonResult !== "ok") {
|
|
1059
1103
|
return { content: [{ type: "text", text: `${TAG} Failed to write users.json: ${authResult.usersError ?? "unknown error"}` }], isError: true };
|
|
1060
1104
|
}
|
|
1105
|
+
// Task 2531 — the observed post-condition, re-read from disk. A binding that
|
|
1106
|
+
// silently failed to land looks identical to one that was never asked for.
|
|
1107
|
+
if (phone !== undefined || telegram !== undefined) {
|
|
1108
|
+
verifyBindingWrite("[admin-add]", userId, { phone, telegram });
|
|
1109
|
+
}
|
|
1061
1110
|
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=${authResult.accountJsonResult} store=account${authResult.accountError ? ` error=${authResult.accountError}` : ""}`);
|
|
1062
1111
|
if (authResult.accountJsonResult === "fail") {
|
|
1063
1112
|
return { content: [{ type: "text", text: `${TAG} users.json updated; account.json write FAILED — manual reconciliation needed: ${authResult.accountError ?? "unknown error"}` }], isError: true };
|
|
@@ -1388,6 +1437,89 @@ eagerTool(server, "admin-update-pin", "Update an existing admin user's PIN. Defa
|
|
|
1388
1437
|
content: [{ type: "text", text: `PIN updated${self ? "" : ` for userId ${userId}`}. The new PIN takes effect on the next login.` }],
|
|
1389
1438
|
};
|
|
1390
1439
|
});
|
|
1440
|
+
lifelineTool(server, "admin-bind", "Bind an admin to their channels by setting the phone and/or Telegram chat id on their users.json entry. This is what lets one answer reach every channel that person holds: without it a WhatsApp or Telegram turn resolves to no admin and the answer reaches only the door it arrived on. Defaults to the calling admin; pass userId (from admin-list) to bind someone else, which additionally requires confirm=true because a binding decides whose identity an inbound resolves to. Sets only the fields supplied; an omitted field is left untouched. A new admin can be born bound by passing the same fields to admin-add.", {
|
|
1441
|
+
userId: z.string().optional().describe("The userId of the admin to bind. Defaults to the caller (the admin invoking this tool)."),
|
|
1442
|
+
phone: z.string().optional().describe("E.164 phone (e.g. +441234567890) binding this admin to their WhatsApp sender."),
|
|
1443
|
+
telegram: z.string().optional().describe("Telegram chat id binding this admin to their Telegram chat (negative for a group)."),
|
|
1444
|
+
confirm: z.boolean().optional().describe("Required (true) only when userId names a DIFFERENT admin than the caller — confirms a deliberate cross-identity binding."),
|
|
1445
|
+
}, async ({ userId: targetUserId, phone, telegram, confirm }) => {
|
|
1446
|
+
if (!ACCOUNT_ID)
|
|
1447
|
+
return refuseNoAccount("admin-bind");
|
|
1448
|
+
const TAG = "[admin-bind]";
|
|
1449
|
+
// A binding decides which sender resolves AS this person, so it is a
|
|
1450
|
+
// credential-class write: the same Task 1573 gate the PIN tools use. Without
|
|
1451
|
+
// it any admin could point their own number at another admin's userId and
|
|
1452
|
+
// have their inbound turns attributed to that person.
|
|
1453
|
+
const callerUserId = verifiedCaller();
|
|
1454
|
+
const resolved = resolveCredentialTarget({ explicitUserId: targetUserId, callerUserId, confirm });
|
|
1455
|
+
if (!resolved.ok) {
|
|
1456
|
+
console.error(`${TAG} result=refused reason=${resolved.error.split(":")[0]}`);
|
|
1457
|
+
return { content: [{ type: "text", text: `${TAG} ${resolved.error}` }], isError: true };
|
|
1458
|
+
}
|
|
1459
|
+
const userId = resolved.target;
|
|
1460
|
+
const label = userId.slice(0, 8);
|
|
1461
|
+
const refuse = (reason, message) => {
|
|
1462
|
+
console.error(`${TAG} userId=${label} result=refused reason=${reason}`);
|
|
1463
|
+
return { content: [{ type: "text", text: `${TAG} ${message}` }], isError: true };
|
|
1464
|
+
};
|
|
1465
|
+
let users;
|
|
1466
|
+
try {
|
|
1467
|
+
users = readUsersJson();
|
|
1468
|
+
}
|
|
1469
|
+
catch (err) {
|
|
1470
|
+
return refuse("users-json-read-failed", err instanceof Error ? err.message : String(err));
|
|
1471
|
+
}
|
|
1472
|
+
let admins;
|
|
1473
|
+
try {
|
|
1474
|
+
const config = await readAccountConfig();
|
|
1475
|
+
admins = (config.admins ?? []);
|
|
1476
|
+
}
|
|
1477
|
+
catch (err) {
|
|
1478
|
+
return refuse("account-config-read-failed", err instanceof Error ? err.message : String(err));
|
|
1479
|
+
}
|
|
1480
|
+
const verdict = classifyAdminBind({ userId, phone, telegram, users, admins });
|
|
1481
|
+
if (verdict.kind !== "ok") {
|
|
1482
|
+
const message = {
|
|
1483
|
+
"nothing-to-set": "Supply phone, telegram, or both.",
|
|
1484
|
+
"bad-phone": "phone must be E.164, e.g. +441234567890.",
|
|
1485
|
+
"bad-telegram": "telegram must be a chat id (digits, optionally negative for a group).",
|
|
1486
|
+
"no-such-user": `User ${userId} not found in users.json.`,
|
|
1487
|
+
"not-an-admin": `User ${userId} is not an admin of this account.`,
|
|
1488
|
+
};
|
|
1489
|
+
return refuse(verdict.kind, message[verdict.kind]);
|
|
1490
|
+
}
|
|
1491
|
+
// The one chokepoint, same as admin-add. `verdict.pin` is the existing hash,
|
|
1492
|
+
// so the credential is carried through untouched rather than rotated.
|
|
1493
|
+
const result = writeAdminEntry({
|
|
1494
|
+
userId,
|
|
1495
|
+
pin: verdict.pin,
|
|
1496
|
+
role: "admin",
|
|
1497
|
+
usersFile: USERS_FILE,
|
|
1498
|
+
accountDir: getAccountDir(),
|
|
1499
|
+
caller: "admin-bind",
|
|
1500
|
+
audit: {
|
|
1501
|
+
actor: callerUserId ?? "unknown",
|
|
1502
|
+
session: process.env.SESSION_NODE_ID,
|
|
1503
|
+
logFile: USERS_AUDIT_LOG,
|
|
1504
|
+
target: userId,
|
|
1505
|
+
explicitUserId: resolved.isCrossIdentity,
|
|
1506
|
+
},
|
|
1507
|
+
phone,
|
|
1508
|
+
telegram,
|
|
1509
|
+
});
|
|
1510
|
+
if (result.usersJsonResult !== "ok") {
|
|
1511
|
+
return refuse("write-failed", `Failed to write users.json: ${result.usersError ?? "unknown error"}`);
|
|
1512
|
+
}
|
|
1513
|
+
if (!verifyBindingWrite(TAG, userId, { phone, telegram })) {
|
|
1514
|
+
return refuse("not-verified", "The write reported success but the value is not on disk.");
|
|
1515
|
+
}
|
|
1516
|
+
const set = [phone !== undefined ? "phone" : null, telegram !== undefined ? "telegram" : null]
|
|
1517
|
+
.filter(Boolean)
|
|
1518
|
+
.join(" and ");
|
|
1519
|
+
return {
|
|
1520
|
+
content: [{ type: "text", text: `${TAG} Bound ${set} for ${label}. A turn from this person now reaches every channel they hold, not only the one it arrived on.` }],
|
|
1521
|
+
};
|
|
1522
|
+
});
|
|
1391
1523
|
lifelineTool(server, "agent-image", "Upload, update, or remove an agent's image. action=set: copy the file at filePath to the agent's assets directory and update config.json with image URL and shape. action=remove: delete the image file and clear the image fields from config.json. imageShape: 'circle' for avatars/icons, 'rounded' for logos.", {
|
|
1392
1524
|
agentSlug: z.string().describe("Agent slug (e.g. 'coaching', 'sales')"),
|
|
1393
1525
|
action: z.enum(["set", "remove"]),
|