@rubytech/create-maxy-code 0.1.244 → 0.1.247
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/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +230 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pin-identity-gate.test.sh +96 -0
- package/payload/platform/plugins/admin/hooks/pin-identity-gate.sh +136 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.js +34 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.js +50 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +9 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/admin-identity-authenticate.d.ts +6 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/admin-identity-authenticate.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/admin-identity-authenticate.js +32 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/admin-identity-authenticate.js.map +1 -0
- package/payload/platform/plugins/admin/skills/investigate/SKILL.md +34 -17
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -16
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +37 -56
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +5 -0
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +5 -0
- package/payload/platform/plugins/admin/skills/update-knowledge/SKILL.md +5 -0
- package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +10 -7
- package/payload/platform/plugins/cloudflare/references/api.md +166 -0
- package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +157 -0
- package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +6 -6
- package/payload/platform/plugins/cloudflare/references/hosting-sites.md +66 -0
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +5 -3
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +72 -0
- package/payload/platform/plugins/docs/references/admin-identity-gate.md +81 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +4 -4
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +1 -0
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +5 -0
- package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +5 -0
- package/payload/platform/scripts/check-skill-frontmatter.mjs +139 -0
- package/payload/platform/scripts/setup-account.sh +21 -8
- package/payload/platform/services/claude-session-manager/dist/admin-identity-audit.d.ts +39 -0
- package/payload/platform/services/claude-session-manager/dist/admin-identity-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/admin-identity-audit.js +133 -0
- package/payload/platform/services/claude-session-manager/dist/admin-identity-audit.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/index.js +17 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +12 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +31 -5
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +2 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +39 -6
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +2 -2
- package/payload/server/{chunk-SRO5RFMV.js → chunk-JMEX5NRX.js} +1 -3
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +261 -153
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +0 -55
- package/payload/platform/templates/agents/public/SOUL.md +0 -19
- package/payload/platform/templates/agents/public/config.json +0 -8
package/package.json
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Public Agent Knowledge Delivery — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make the public agent's `KNOWLEDGE.md` reach its spawn prompt, refuse a dud (empty/missing SOUL or KNOWLEDGE) public spawn with a distinct logged reason, and stop seeding/reading the dead public `SOUL`/`config` templates and `plugins` config field.
|
|
6
|
+
|
|
7
|
+
**Architecture:** The only spawn-prompt path is `composeAppendSystemPrompt` in `claude-session-manager`. It reads `agents/<role>/{IDENTITY,SOUL}.md`; we add a public-only `KNOWLEDGE.md` read rendered as `<knowledge>` after `<soul>`, with strict empty/missing refusal. `pty-spawner` maps the new refusal reasons and emits the per-spawn byte-count line. The installer stops manufacturing a public agent on fresh install but re-syncs the Rubytech IDENTITY into existing agent dirs on upgrade. The UI config reader drops the unused `plugins` field. The admin skill doc is corrected to match.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (Node, vitest), bash (`setup-account.sh`), markdown (SKILL.md).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: `system-prompt.ts` — read KNOWLEDGE for public, distinct refusal reasons
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `services/claude-session-manager/src/system-prompt.ts`
|
|
17
|
+
- Test: `services/claude-session-manager/src/__tests__/system-prompt.test.ts`
|
|
18
|
+
|
|
19
|
+
**Behaviours:**
|
|
20
|
+
- `AppendBlockResult` ok:true gains `knowledgeBytes: number`.
|
|
21
|
+
- `AppendBlockResult` ok:false `reason` union gains `'soul-empty' | 'knowledge-missing' | 'knowledge-empty'`.
|
|
22
|
+
- `renderAppendBlock` gains a `knowledgeContent: string | null` param; when non-null, render `<knowledge>\n{content}\n</knowledge>` immediately after `</soul>`, before `<about-owner>`; return `knowledgeBytes`. Null ⇒ no block, no bytes (admin + self-test + drift unchanged).
|
|
23
|
+
- `composeAppendSystemPrompt`: IDENTITY failure → `identity-unresolved` (all roles, unchanged). SOUL failure → `identity-unresolved` for admin (unchanged); for `role==='public'` → `soul-empty`. When `role==='public'`, read `<roleDir>/KNOWLEDGE.md` via `readIdentityFile`: detail ending `:empty` → `knowledge-empty`; any other failure (ENOENT/unreadable) → `knowledge-missing`. Pass content to `renderAppendBlock`; admin passes `null`.
|
|
24
|
+
|
|
25
|
+
- [ ] **Step 1: Write failing tests** — append to `system-prompt.test.ts`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
describe('composeAppendSystemPrompt — public KNOWLEDGE delivery', () => {
|
|
29
|
+
function makePublic(seed: { identity?: string | null; soul?: string | null; knowledge?: string | null }): string {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), 'task618-account-'))
|
|
31
|
+
const roleDir = join(dir, 'agents', 'public')
|
|
32
|
+
mkdirSync(roleDir, { recursive: true })
|
|
33
|
+
if (typeof seed.identity === 'string') writeFileSync(join(roleDir, 'IDENTITY.md'), seed.identity)
|
|
34
|
+
if (typeof seed.soul === 'string') writeFileSync(join(roleDir, 'SOUL.md'), seed.soul)
|
|
35
|
+
if (typeof seed.knowledge === 'string') writeFileSync(join(roleDir, 'KNOWLEDGE.md'), seed.knowledge)
|
|
36
|
+
return dir
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('public + non-empty KNOWLEDGE ⇒ <knowledge> verbatim, ordered after <soul>, knowledgeBytes>0', () => {
|
|
40
|
+
const knowledge = '# Knowledge\nWe open 9-5. Sale price £450k.\n'
|
|
41
|
+
const accountDir = makePublic({ identity: 'I', soul: 'S', knowledge })
|
|
42
|
+
try {
|
|
43
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
44
|
+
expect(result.ok).toBe(true)
|
|
45
|
+
if (!result.ok) return
|
|
46
|
+
expect(result.block).toContain(`<knowledge>\n${knowledge}\n</knowledge>`)
|
|
47
|
+
expect(result.block.indexOf('<knowledge>')).toBeGreaterThan(result.block.indexOf('</soul>'))
|
|
48
|
+
expect(result.knowledgeBytes).toBe(Buffer.byteLength(knowledge, 'utf8'))
|
|
49
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('public + missing KNOWLEDGE ⇒ ok:false reason=knowledge-missing', () => {
|
|
53
|
+
const accountDir = makePublic({ identity: 'I', soul: 'S' })
|
|
54
|
+
try {
|
|
55
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
56
|
+
expect(result.ok).toBe(false)
|
|
57
|
+
if (result.ok) return
|
|
58
|
+
expect(result.reason).toBe('knowledge-missing')
|
|
59
|
+
expect(result.detail).toContain('KNOWLEDGE.md')
|
|
60
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('public + empty KNOWLEDGE ⇒ ok:false reason=knowledge-empty', () => {
|
|
64
|
+
const accountDir = makePublic({ identity: 'I', soul: 'S', knowledge: '' })
|
|
65
|
+
try {
|
|
66
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
67
|
+
expect(result.ok).toBe(false)
|
|
68
|
+
if (result.ok) return
|
|
69
|
+
expect(result.reason).toBe('knowledge-empty')
|
|
70
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('public + empty SOUL ⇒ ok:false reason=soul-empty', () => {
|
|
74
|
+
const accountDir = makePublic({ identity: 'I', soul: '', knowledge: 'K' })
|
|
75
|
+
try {
|
|
76
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
77
|
+
expect(result.ok).toBe(false)
|
|
78
|
+
if (result.ok) return
|
|
79
|
+
expect(result.reason).toBe('soul-empty')
|
|
80
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('public + empty IDENTITY ⇒ identity-unresolved (unchanged)', () => {
|
|
84
|
+
const accountDir = makePublic({ identity: '', soul: 'S', knowledge: 'K' })
|
|
85
|
+
try {
|
|
86
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
87
|
+
expect(result.ok).toBe(false)
|
|
88
|
+
if (result.ok) return
|
|
89
|
+
expect(result.reason).toBe('identity-unresolved')
|
|
90
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('admin + KNOWLEDGE.md on disk ⇒ no <knowledge> block', () => {
|
|
94
|
+
const accountDir = makeAccountDir({ admin: { identity: 'I', soul: 'S' } })
|
|
95
|
+
writeFileSync(join(accountDir, 'agents', 'admin', 'KNOWLEDGE.md'), 'admin-knowledge')
|
|
96
|
+
try {
|
|
97
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'admin' })
|
|
98
|
+
expect(result.ok).toBe(true)
|
|
99
|
+
if (!result.ok) return
|
|
100
|
+
expect(result.block).not.toContain('<knowledge>')
|
|
101
|
+
expect(result.block).not.toContain('admin-knowledge')
|
|
102
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('snapshot: full composed public prompt with a sample KNOWLEDGE.md', () => {
|
|
106
|
+
const accountDir = makePublic({
|
|
107
|
+
identity: '# Identity\nYou speak for the business.',
|
|
108
|
+
soul: '# Soul\nWarm, brief.',
|
|
109
|
+
knowledge: '# Knowledge\nOpen 9-5. Contact 01234 567890.',
|
|
110
|
+
})
|
|
111
|
+
try {
|
|
112
|
+
const result = composeAppendSystemPrompt(HOST, { accountDir, role: 'public' })
|
|
113
|
+
expect(result.ok).toBe(true)
|
|
114
|
+
if (!result.ok) return
|
|
115
|
+
expect(result.block).toMatchSnapshot()
|
|
116
|
+
} finally { rmSync(accountDir, { recursive: true, force: true }) }
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 2: Run, verify fail** — `npx vitest run src/__tests__/system-prompt.test.ts` → FAIL (knowledge not read).
|
|
122
|
+
|
|
123
|
+
- [ ] **Step 3: Implement** in `system-prompt.ts`:
|
|
124
|
+
- Add `knowledgeBytes: number` to ok:true; extend ok:false reason union.
|
|
125
|
+
- Add `knowledgeContent: string | null` param to `renderAppendBlock`; insert `<knowledge>` block after `</soul>` push, before `<about-owner>`; track + return `knowledgeBytes`.
|
|
126
|
+
- Update both internal `renderAppendBlock` callers (`runIdentityDriftAssertion`, `runSystemPromptSelfTest`) to pass `null`.
|
|
127
|
+
- In `composeAppendSystemPrompt`: on SOUL failure, `return { ok:false, reason: sources.role === 'public' ? 'soul-empty' : 'identity-unresolved', detail: soul.detail }`. When `role==='public'`, read `KNOWLEDGE.md`; on failure `return { ok:false, reason: knowledge.detail.endsWith(':empty') ? 'knowledge-empty' : 'knowledge-missing', detail: knowledge.detail }`. Pass `role==='public' ? knowledge.content : null` to `renderAppendBlock`; set `knowledgeBytes` accordingly (0 for admin).
|
|
128
|
+
|
|
129
|
+
- [ ] **Step 4: Run, verify pass** — `npx vitest run src/__tests__/system-prompt.test.ts` → PASS. Review the new snapshot.
|
|
130
|
+
|
|
131
|
+
- [ ] **Step 5: Commit** — `feat(public-agent): read KNOWLEDGE.md into public spawn prompt with strict refusal`
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Task 2: `pty-spawner.ts` — surface refusal reasons + per-spawn compose line
|
|
136
|
+
|
|
137
|
+
**Files:**
|
|
138
|
+
- Modify: `services/claude-session-manager/src/pty-spawner.ts` (`SpawnFailureReason` ~351; compose handling ~1292-1315)
|
|
139
|
+
|
|
140
|
+
**Behaviours:**
|
|
141
|
+
- `SpawnFailureReason` gains `'soul-empty' | 'knowledge-missing' | 'knowledge-empty'`.
|
|
142
|
+
- In the `!composed.ok` branch, for the three new reasons emit `[pty-spawn] spawn-failed reason=<r> role=public slug=<agentSlug>` and return `{ ok:false, reason:<r>, stderrTail: composed.detail }`. The existing `identity-unresolved` path is unchanged.
|
|
143
|
+
- Track `knowledge` in `composedBytes`. On public success emit `[pty-spawn] op=public-prompt-compose role=public slug=<agentSlug> identityBytes=N soulBytes=N knowledgeBytes=N` (in addition to the existing `compose-system-prompt-ok` line, which gains `knowledgeBytes`).
|
|
144
|
+
|
|
145
|
+
- [ ] **Step 1: Implement** — extend the union; in the failure branch add a `case` for the three reasons before the identity fallthrough; add `knowledge: composed.knowledgeBytes` to `composedBytes`; append `knowledgeBytes=${composed.knowledgeBytes}` to the `compose-system-prompt-ok` log; after that log, `if (args.role === 'public') deps.logger(\`op=public-prompt-compose role=public slug=${agentSlug} identityBytes=${composed.identityBytes} soulBytes=${composed.soulBytes} knowledgeBytes=${composed.knowledgeBytes}\`)`.
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 2: Verify** — `npx vitest run` for csm (no test directly asserts these strings; the build + existing suite must stay green). Confirm tsc has no error on the union/result fields.
|
|
148
|
+
|
|
149
|
+
- [ ] **Step 3: Commit** — `feat(public-agent): map knowledge/soul refusal reasons + emit public-prompt-compose line`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### Task 3: Templates — delete dead public SOUL + config
|
|
154
|
+
|
|
155
|
+
**Files:**
|
|
156
|
+
- Delete: `templates/agents/public/SOUL.md`, `templates/agents/public/config.json`
|
|
157
|
+
- Keep: `templates/agents/public/IDENTITY.md`
|
|
158
|
+
|
|
159
|
+
- [ ] **Step 1:** `git rm templates/agents/public/SOUL.md templates/agents/public/config.json`
|
|
160
|
+
- [ ] **Step 2: Verify no live reader** — `grep -rIn 'agents/public/SOUL\|agents/public/config' --include=*.ts --include=*.sh .` returns only `setup-account.sh` lines removed in Task 4. (`smoke-boot-services.sh` and `verify-doc-impl.sh` copy/grep IDENTITY only.)
|
|
161
|
+
- [ ] **Step 3: Commit** with Task 4 (template deletion is meaningless without the seed removal).
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Task 4: `setup-account.sh` — no fresh-install public agent; re-sync IDENTITY for existing
|
|
166
|
+
|
|
167
|
+
**Files:**
|
|
168
|
+
- Modify: `scripts/setup-account.sh` (mkdir ~38; IDENTITY block ~181-184; SOUL/config seed ~221-224)
|
|
169
|
+
|
|
170
|
+
**Behaviours:**
|
|
171
|
+
- Drop `"$ACCOUNT_DIR/agents/public"` from the line-38 `mkdir -p`.
|
|
172
|
+
- Remove the public IDENTITY overwrite (lines 183-184) and the public SOUL/config seed (lines 221-224).
|
|
173
|
+
- Add, in the agent-identities section, a loop: for each `"$ACCOUNT_DIR/agents"/*/` whose basename ≠ `admin` and which already contains `IDENTITY.md`, overwrite `IDENTITY.md` from `"$TEMPLATES_DIR/agents/public/IDENTITY.md"`; echo a re-sync line. Never create dirs; never write SOUL/KNOWLEDGE/config. Admin block untouched.
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 1: Implement** the three edits.
|
|
176
|
+
- [ ] **Step 2: Verify fresh install** — run `setup-account.sh` against a scratch `ACCOUNT_DIR` with no `agents/public`; assert no `agents/public/` is created and `agents/admin/IDENTITY.md` exists.
|
|
177
|
+
- [ ] **Step 3: Verify upgrade** — scratch `ACCOUNT_DIR` pre-seeded with `agents/sales/IDENTITY.md` (stale text) + `agents/sales/SOUL.md`; run; assert `agents/sales/IDENTITY.md` now equals the template and `agents/sales/SOUL.md` is untouched.
|
|
178
|
+
- [ ] **Step 4: Commit** (with Task 3) — `refactor(installer): public agents author client-side; re-sync IDENTITY for existing dirs only`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Task 5: `account.ts` — drop the dead `plugins` config field
|
|
183
|
+
|
|
184
|
+
**Files:**
|
|
185
|
+
- Modify: `ui/app/lib/claude-agent/account.ts` (`ResolvedAgentConfig` ~277; reader ~304,324; return ~395)
|
|
186
|
+
- Test: `ui/app/lib/claude-agent/__tests__/spawn-context.test.ts` (trim `plugins` assertions only)
|
|
187
|
+
|
|
188
|
+
**Behaviours:**
|
|
189
|
+
- Remove `plugins: string[] | null` from `ResolvedAgentConfig`; remove `let plugins`, the `parsed.plugins` read, and `plugins` from the return object. Leave `knowledgeKeywords` and the budget-breakdown `plugins: 0` token field intact.
|
|
190
|
+
|
|
191
|
+
- [ ] **Step 1: Find & update the test** — `grep -n 'plugins' ui/app/lib/claude-agent/__tests__/spawn-context.test.ts`; remove only the assertions reading `config.plugins`; add/confirm a case that a public `config.json` lacking `plugins` resolves and `knowledgeKeywords` is still read.
|
|
192
|
+
- [ ] **Step 2: Run, verify fail/observe** — `npx vitest run ui/app/lib/claude-agent/__tests__/spawn-context.test.ts`.
|
|
193
|
+
- [ ] **Step 3: Implement** the field removal in `account.ts`.
|
|
194
|
+
- [ ] **Step 4: Run, verify pass** — same vitest command → PASS.
|
|
195
|
+
- [ ] **Step 5: Commit** — `refactor(public-agent): drop unused plugins field from resolved agent config`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Task 6: `public-agent-manager/SKILL.md` — correct the doc
|
|
200
|
+
|
|
201
|
+
**Files:**
|
|
202
|
+
- Modify: `plugins/admin/skills/public-agent-manager/SKILL.md`
|
|
203
|
+
|
|
204
|
+
**Behaviours (scope item 6):**
|
|
205
|
+
- (a) Table: `KNOWLEDGE.md` Required → Yes; `SOUL.md` purpose → personality + role (basic public info, capture the visitor's contact details for human follow-up — the chat equivalent of the landing-page contact form). Add a line: SOUL.md and KNOWLEDGE.md are authored entirely client-side and must be non-empty before the agent goes active; an empty one is refused at spawn. KNOWLEDGE = the business's public-facing facts at landing-page detail.
|
|
206
|
+
- (b) Create step: IDENTITY.md is copied from the Rubytech template verbatim, not drafted/edited.
|
|
207
|
+
- (c) Remove the `plugins` key from the `config.json` example; remove "selected plugins" from the table; delete the "Plugin Selection" section; remove the plugin-selection Create step and renumber the Create steps + every cross-reference (`Step 4/5/6/7`, `All other steps (4, 8-12)`). Update the template-create plugin pre-select bullet (plugins gone) and note IDENTITY is the fixed Rubytech template.
|
|
208
|
+
- (d) Ensure no reference to a SOUL/config seed template remains.
|
|
209
|
+
- Keep: `knowledgeKeywords` field, the create-form field, keyword-subscription and direct-tagging sections, projection steps.
|
|
210
|
+
|
|
211
|
+
- [ ] **Step 1: Implement** the edits above.
|
|
212
|
+
- [ ] **Step 2: Verify** — `grep -n 'plugins' SKILL.md` shows no `plugins` config key or Plugin Selection section (knowledgeKeywords/tagging prose may remain); Create steps are sequentially numbered with consistent cross-refs.
|
|
213
|
+
- [ ] **Step 3: Commit** — `docs(public-agent): KNOWLEDGE required, IDENTITY templated, drop plugins from skill`
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Task 7: Verify — build, bundle, full suites
|
|
218
|
+
|
|
219
|
+
- [ ] **Step 1:** `npm run build:claude-session-manager` → no tsc errors.
|
|
220
|
+
- [ ] **Step 2:** Full csm suite `cd services/claude-session-manager && npx vitest run` → green (esp. `public-identity-template`, `pty-spawner-channels`).
|
|
221
|
+
- [ ] **Step 3:** `npx vitest run ui/app/lib/claude-agent/__tests__/` → green.
|
|
222
|
+
- [ ] **Step 4:** Bundle the installer payload (`maxy-code/packages/create-maxy-code` `npm run bundle`) → no runtime errors; confirm `templates/agents/public/` in the payload contains only `IDENTITY.md`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Self-Review
|
|
227
|
+
|
|
228
|
+
- **Spec coverage:** scope 1→T1, 2→T2, 3→T3, 4→T4, 5→T5, 6→T6; verification + observability→T1/T2/T7. All covered.
|
|
229
|
+
- **Out of scope (untouched):** `knowledgeKeywords` machinery, projection routes, the migrated Real Agent agent's KNOWLEDGE authoring, Pi hand-patched files.
|
|
230
|
+
- **Type consistency:** `knowledgeBytes` and the three reasons named identically in T1 (composer) and T2 (spawner). `renderAppendBlock` `knowledgeContent` param threaded through all three callers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "admin",
|
|
3
|
-
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit),
|
|
3
|
+
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective), and admin-identity-authenticate (native-CC PIN identity gate — the operator submits their PIN as the first action of every session to bind their authenticated userId; the PreToolUse pin-identity-gate hook blocks all other tools until this succeeds) for managing the Maxy platform.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin
|
|
3
3
|
surface: platform
|
|
4
|
-
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit),
|
|
4
|
+
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective), and admin-identity-authenticate (native-CC PIN identity gate — the operator submits their PIN as the first action of every session to bind their authenticated userId; the PreToolUse pin-identity-gate hook blocks all other tools until this succeeds) for managing the Maxy platform."
|
|
5
5
|
tools:
|
|
6
6
|
- name: system-status
|
|
7
7
|
publicAllowlist: false
|
|
@@ -75,6 +75,9 @@ tools:
|
|
|
75
75
|
- name: session-retrospective-mark-complete
|
|
76
76
|
publicAllowlist: false
|
|
77
77
|
adminAllowlist: true
|
|
78
|
+
- name: admin-identity-authenticate
|
|
79
|
+
publicAllowlist: false
|
|
80
|
+
adminAllowlist: true
|
|
78
81
|
hidden:
|
|
79
82
|
- remote-auth-status
|
|
80
83
|
- remote-auth-set-password
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for pin-identity-gate.sh (Task 619).
|
|
3
|
+
#
|
|
4
|
+
# Covers:
|
|
5
|
+
# 1. Non-admin role → ALLOW (no-op)
|
|
6
|
+
# 2. admin, unauthenticated, ordinary tool → BLOCK (op=pin-prompted)
|
|
7
|
+
# 3. admin, already authenticated (state file) → ALLOW
|
|
8
|
+
# 4. admin, auth tool, endpoint match → ALLOW + state written (op=pin-bound)
|
|
9
|
+
# 5. admin, auth tool, endpoint miss → BLOCK (op=session-terminated)
|
|
10
|
+
# 6. admin, auth tool, endpoint unavailable → BLOCK (op=pin-unavailable)
|
|
11
|
+
# 7. admin, auth tool, endpoint unreachable → BLOCK (op=pin-error)
|
|
12
|
+
#
|
|
13
|
+
# The validate endpoint is stubbed via a `curl` shim on PATH whose response is
|
|
14
|
+
# driven by $FAKE_VALIDATE_KIND.
|
|
15
|
+
|
|
16
|
+
set -u
|
|
17
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/pin-identity-gate.sh"
|
|
18
|
+
[ -x "$HOOK" ] || { echo "FAIL: $HOOK not executable" >&2; exit 1; }
|
|
19
|
+
|
|
20
|
+
TMP_DIR=$(mktemp -d)
|
|
21
|
+
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
22
|
+
mkdir -p "$TMP_DIR/bin" "$TMP_DIR/state"
|
|
23
|
+
|
|
24
|
+
# curl shim: emit a canned validate response based on $FAKE_VALIDATE_KIND.
|
|
25
|
+
cat > "$TMP_DIR/bin/curl" <<'SHIM'
|
|
26
|
+
#!/usr/bin/env bash
|
|
27
|
+
case "${FAKE_VALIDATE_KIND:-}" in
|
|
28
|
+
match) echo '{"kind":"match","userId":"user-0001"}' ;;
|
|
29
|
+
miss) echo '{"kind":"miss"}' ;;
|
|
30
|
+
unavailable) echo '{"kind":"unavailable"}' ;;
|
|
31
|
+
corrupt) echo '{"kind":"corrupt"}' ;;
|
|
32
|
+
unreachable) echo '' ;;
|
|
33
|
+
*) echo '' ;;
|
|
34
|
+
esac
|
|
35
|
+
SHIM
|
|
36
|
+
chmod +x "$TMP_DIR/bin/curl"
|
|
37
|
+
|
|
38
|
+
AUTH_TOOL='mcp__plugin_admin_admin__admin-identity-authenticate'
|
|
39
|
+
PASS=0; FAIL=0
|
|
40
|
+
|
|
41
|
+
# run <role> <kind> <stdin-json> [specialist] ; sets EXIT and ERR
|
|
42
|
+
run() {
|
|
43
|
+
ERR=$(printf '%s' "$3" | \
|
|
44
|
+
PATH="$TMP_DIR/bin:$PATH" \
|
|
45
|
+
MAXY_SESSION_ROLE="$1" \
|
|
46
|
+
MAXY_SPECIALIST="${4:-}" \
|
|
47
|
+
FAKE_VALIDATE_KIND="$2" \
|
|
48
|
+
MAXY_IDENTITY_GATE_STATE_DIR="$TMP_DIR/state" \
|
|
49
|
+
MAXY_IDENTITY_VALIDATE_URL="http://stub/validate" \
|
|
50
|
+
bash "$HOOK" 2>&1 >/dev/null)
|
|
51
|
+
EXIT=$?
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
check() { # <label> <want-exit> <grep-or-->
|
|
55
|
+
if [ "$EXIT" != "$2" ]; then echo "FAIL[$1]: exit=$EXIT want=$2 err=$ERR"; FAIL=$((FAIL+1)); return; fi
|
|
56
|
+
if [ "$3" != "-" ] && ! printf '%s' "$ERR" | grep -q "$3"; then echo "FAIL[$1]: missing '$3' in: $ERR"; FAIL=$((FAIL+1)); return; fi
|
|
57
|
+
echo "PASS[$1]"; PASS=$((PASS+1))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# 1. non-admin → allow
|
|
61
|
+
run public '' '{"session_id":"s1","tool_name":"Bash","tool_input":{}}'
|
|
62
|
+
check non-admin 0 -
|
|
63
|
+
|
|
64
|
+
# 1b. admin but specialist subagent (MAXY_SPECIALIST set) → allow (no-op)
|
|
65
|
+
run admin '' '{"session_id":"s1b","tool_name":"Bash","tool_input":{}}' database-operator
|
|
66
|
+
check specialist-exempt 0 -
|
|
67
|
+
|
|
68
|
+
# 2. admin unauth, ordinary tool → block
|
|
69
|
+
run admin '' '{"session_id":"s2","tool_name":"Bash","tool_input":{}}'
|
|
70
|
+
check unauth-block 2 "op=pin-prompted"
|
|
71
|
+
|
|
72
|
+
# 3. admin, authenticated (state file present) → allow
|
|
73
|
+
echo '{"userId":"user-0001"}' > "$TMP_DIR/state/s3.json"
|
|
74
|
+
run admin '' '{"session_id":"s3","tool_name":"Bash","tool_input":{}}'
|
|
75
|
+
check authed-allow 0 -
|
|
76
|
+
|
|
77
|
+
# 4. admin, auth tool, match → allow + state written + pin-bound
|
|
78
|
+
run admin match "{\"session_id\":\"s4\",\"tool_name\":\"$AUTH_TOOL\",\"tool_input\":{\"pin\":\"1234\"}}"
|
|
79
|
+
check match-allow 0 "op=pin-bound"
|
|
80
|
+
[ -f "$TMP_DIR/state/s4.json" ] && { echo "PASS[match-state-written]"; PASS=$((PASS+1)); } || { echo "FAIL[match-state-written]"; FAIL=$((FAIL+1)); }
|
|
81
|
+
|
|
82
|
+
# 5. admin, auth tool, miss → block + terminate
|
|
83
|
+
run admin miss "{\"session_id\":\"s5\",\"tool_name\":\"$AUTH_TOOL\",\"tool_input\":{\"pin\":\"9999\"}}"
|
|
84
|
+
check miss-block 2 "op=session-terminated"
|
|
85
|
+
|
|
86
|
+
# 6. admin, auth tool, unavailable → block
|
|
87
|
+
run admin unavailable "{\"session_id\":\"s6\",\"tool_name\":\"$AUTH_TOOL\",\"tool_input\":{\"pin\":\"1\"}}"
|
|
88
|
+
check unavailable-block 2 "op=pin-unavailable"
|
|
89
|
+
|
|
90
|
+
# 7. admin, auth tool, unreachable endpoint → block (fail-closed)
|
|
91
|
+
run admin unreachable "{\"session_id\":\"s7\",\"tool_name\":\"$AUTH_TOOL\",\"tool_input\":{\"pin\":\"1\"}}"
|
|
92
|
+
check unreachable-block 2 "op=pin-error"
|
|
93
|
+
|
|
94
|
+
echo "---"
|
|
95
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
96
|
+
[ "$FAIL" = 0 ] || exit 1
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Native-CC admin PIN identity gate (Task 619).
|
|
3
|
+
#
|
|
4
|
+
# PreToolUse hook, registered under the universal (no-matcher) entry so it fires
|
|
5
|
+
# for EVERY tool call on a native-CC admin session. It denies all tools until the
|
|
6
|
+
# operator authenticates: the agent must call `admin-identity-authenticate` with a
|
|
7
|
+
# PIN that the loopback validate endpoint matches against users.json. On match the
|
|
8
|
+
# hook records per-session auth state (keyed by the conversation `session_id` it
|
|
9
|
+
# reads from stdin) and lets every subsequent tool through. Enforcement is here —
|
|
10
|
+
# a model that skips the prompt cannot reach any other tool.
|
|
11
|
+
#
|
|
12
|
+
# Scope: only native loopback admin sessions (MAXY_SESSION_ROLE=admin). Specialist
|
|
13
|
+
# subagents dispatched via the Task tool share the parent conversation's
|
|
14
|
+
# session_id, so they pass once the operator session is authenticated.
|
|
15
|
+
#
|
|
16
|
+
# Protocol: exit 0 = allow, exit 2 = block (stderr message shown to the agent),
|
|
17
|
+
# matching the sibling gate hooks. Fail-closed on terminal stdin.
|
|
18
|
+
#
|
|
19
|
+
# Lifecycle (one [admin-identity] line per step, correlated by sessionId):
|
|
20
|
+
# op=loopback-spawn-received → op=pin-prompted → (op=pin-bound source=pin |
|
|
21
|
+
# op=pin-miss → op=session-terminated) ; the MCP tool emits op=about-owner-loaded.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
if [ -t 0 ]; then
|
|
26
|
+
echo "Blocked: pin-identity-gate received no stdin (cannot inspect tool call). Failing closed." >&2
|
|
27
|
+
exit 2
|
|
28
|
+
fi
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
|
|
31
|
+
# Only native loopback admin OPERATOR sessions are in scope:
|
|
32
|
+
# - MAXY_SESSION_ROLE=admin excludes public/access sessions.
|
|
33
|
+
# - MAXY_SPECIALIST empty excludes pty-spawned specialist/recorder sessions,
|
|
34
|
+
# which carry role=admin but are not human operators and never enter a PIN
|
|
35
|
+
# (mirrors the recursion guard the other admin hooks use). Task-tool
|
|
36
|
+
# specialist subagents share the operator's session_id + empty MAXY_SPECIALIST,
|
|
37
|
+
# so they pass via the operator's auth-state file once it is bound.
|
|
38
|
+
[ "${MAXY_SESSION_ROLE:-}" = "admin" ] || exit 0
|
|
39
|
+
[ -z "${MAXY_SPECIALIST:-}" ] || exit 0
|
|
40
|
+
|
|
41
|
+
# ----- Parse fields from stdin (python3, robust on nested tool_input) -------
|
|
42
|
+
SESSION_ID=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
|
|
43
|
+
try: d=json.load(sys.stdin)
|
|
44
|
+
except Exception: d={}
|
|
45
|
+
print(d.get("session_id","") or "")' 2>/dev/null)
|
|
46
|
+
TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
|
|
47
|
+
try: d=json.load(sys.stdin)
|
|
48
|
+
except Exception: d={}
|
|
49
|
+
print(d.get("tool_name","") or "")' 2>/dev/null)
|
|
50
|
+
|
|
51
|
+
SID8="${SESSION_ID:0:8}"; [ -z "$SID8" ] && SID8='-'
|
|
52
|
+
log() { echo "[admin-identity] $1" >&2; }
|
|
53
|
+
|
|
54
|
+
# ----- Resolve account/state dir -------------------------------------------
|
|
55
|
+
# The account dir basename IS the ACCOUNT_ID (resolve-account-dir.sh), and the
|
|
56
|
+
# rc child env carries ACCOUNT_ID, so resolve deterministically rather than
|
|
57
|
+
# `find | head -1` — which does not skip dotfiles and could pick the sibling
|
|
58
|
+
# `data/accounts/.trash/`, diverging from the manager's `spawnCwd` (= the
|
|
59
|
+
# account dir) and stranding state the standing audit can never reconcile.
|
|
60
|
+
HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
61
|
+
PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
|
|
62
|
+
ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
|
|
63
|
+
ACCOUNT_DIR=""
|
|
64
|
+
if [ -n "${ACCOUNT_ID:-}" ]; then
|
|
65
|
+
ACCOUNT_DIR="${ACCOUNTS_DIR}/${ACCOUNT_ID}"
|
|
66
|
+
elif [ -d "$ACCOUNTS_DIR" ]; then
|
|
67
|
+
# Defensive fallback only when ACCOUNT_ID is unset — exclude dot-entries.
|
|
68
|
+
ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | head -1)
|
|
69
|
+
fi
|
|
70
|
+
STATE_DIR="${MAXY_IDENTITY_GATE_STATE_DIR:-${ACCOUNT_DIR}/state/admin-identity}"
|
|
71
|
+
STATE_FILE="${STATE_DIR}/${SESSION_ID}.json"
|
|
72
|
+
|
|
73
|
+
AUTH_TOOL='mcp__plugin_admin_admin__admin-identity-authenticate'
|
|
74
|
+
VALIDATE_URL="${MAXY_IDENTITY_VALIDATE_URL:-http://127.0.0.1:${MAXY_UI_INTERNAL_PORT:-0}/api/admin/identity/validate}"
|
|
75
|
+
|
|
76
|
+
# Already authenticated for this conversation → pass everything through.
|
|
77
|
+
if [ -n "$SESSION_ID" ] && [ -f "$STATE_FILE" ]; then
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
log "op=loopback-spawn-received sessionId=${SID8}"
|
|
82
|
+
|
|
83
|
+
# ----- The authenticate tool: validate, bind on match, terminate on miss ----
|
|
84
|
+
if [ "$TOOL_NAME" = "$AUTH_TOOL" ]; then
|
|
85
|
+
PIN=$(printf '%s' "$INPUT" | python3 -c 'import json,sys
|
|
86
|
+
try: d=json.load(sys.stdin)
|
|
87
|
+
except Exception: d={}
|
|
88
|
+
ti=d.get("tool_input") or {}
|
|
89
|
+
print(ti.get("pin","") or "")' 2>/dev/null)
|
|
90
|
+
|
|
91
|
+
REQ=$(python3 -c 'import json,sys
|
|
92
|
+
print(json.dumps({"pin": sys.argv[1], "sessionId": sys.argv[2]}))' "$PIN" "$SESSION_ID" 2>/dev/null)
|
|
93
|
+
RESP=$(printf '%s' "$REQ" | curl -s --max-time 5 -X POST -H 'content-type: application/json' -d @- "$VALIDATE_URL" 2>/dev/null)
|
|
94
|
+
KIND=$(printf '%s' "$RESP" | python3 -c 'import json,sys
|
|
95
|
+
try: print((json.load(sys.stdin) or {}).get("kind","") or "")
|
|
96
|
+
except Exception: print("")' 2>/dev/null)
|
|
97
|
+
|
|
98
|
+
case "$KIND" in
|
|
99
|
+
match)
|
|
100
|
+
USERID=$(printf '%s' "$RESP" | python3 -c 'import json,sys
|
|
101
|
+
print(json.load(sys.stdin).get("userId","") or "")' 2>/dev/null)
|
|
102
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
103
|
+
printf '{"userId":%s,"ts":"%s"}' \
|
|
104
|
+
"$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$USERID")" \
|
|
105
|
+
"$(date -u +%FT%TZ)" > "$STATE_FILE" 2>/dev/null
|
|
106
|
+
log "op=pin-bound sessionId=${SID8} userId=${USERID:0:8} source=pin authSurface=loopback"
|
|
107
|
+
exit 0
|
|
108
|
+
;;
|
|
109
|
+
miss)
|
|
110
|
+
log "op=pin-miss sessionId=${SID8} reason=no-matching-user"
|
|
111
|
+
log "op=session-terminated sessionId=${SID8} reason=pin-auth-failed"
|
|
112
|
+
echo "PIN did not match. This session is being terminated. Start a new session and enter a valid PIN." >&2
|
|
113
|
+
exit 2
|
|
114
|
+
;;
|
|
115
|
+
unavailable)
|
|
116
|
+
log "op=pin-unavailable sessionId=${SID8} reason=users-json-empty"
|
|
117
|
+
echo "No admin PIN is configured for this install. Complete onboarding before using the admin agent." >&2
|
|
118
|
+
exit 2
|
|
119
|
+
;;
|
|
120
|
+
corrupt)
|
|
121
|
+
log "op=pin-error sessionId=${SID8} reason=users-json-corrupt"
|
|
122
|
+
echo "Admin user configuration is corrupt. Re-run the seed script before authenticating." >&2
|
|
123
|
+
exit 2
|
|
124
|
+
;;
|
|
125
|
+
*)
|
|
126
|
+
log "op=pin-error sessionId=${SID8} reason=validate-unreachable"
|
|
127
|
+
echo "Identity validation is unavailable; cannot proceed without authentication." >&2
|
|
128
|
+
exit 2
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ----- Unauthenticated and not the auth tool → block -----------------------
|
|
134
|
+
log "op=pin-prompted sessionId=${SID8}"
|
|
135
|
+
echo "Authenticate first: call admin-identity-authenticate with the operator's PIN. No other tool is reachable until a matching PIN is bound for this session." >&2
|
|
136
|
+
exit 2
|
package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-identity-authenticate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/admin-identity-authenticate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { runAuthenticate } from "../tools/admin-identity-authenticate.js";
|
|
3
|
+
function fetchReturning(json) {
|
|
4
|
+
return vi.fn(async () => ({ json: async () => json }));
|
|
5
|
+
}
|
|
6
|
+
describe("runAuthenticate", () => {
|
|
7
|
+
it("returns the operator name and profile on match", async () => {
|
|
8
|
+
const f = fetchReturning({ kind: "match", userId: "u-1", userName: "Ada", aboutOwner: { ok: true, body: "PROFILE" } });
|
|
9
|
+
const res = await runAuthenticate("1234", { fetchImpl: f, uiPort: "1" });
|
|
10
|
+
expect(res).toContain("Ada");
|
|
11
|
+
expect(res).toContain("PROFILE");
|
|
12
|
+
});
|
|
13
|
+
it("returns Invalid PIN on miss", async () => {
|
|
14
|
+
const f = fetchReturning({ kind: "miss" });
|
|
15
|
+
const res = await runAuthenticate("9999", { fetchImpl: f, uiPort: "1" });
|
|
16
|
+
expect(res).toBe("Invalid PIN.");
|
|
17
|
+
});
|
|
18
|
+
it("omits the profile when aboutOwner is unresolved but still authenticates", async () => {
|
|
19
|
+
const f = fetchReturning({ kind: "match", userId: "u-1", userName: "Ada", aboutOwner: { ok: false, reason: "profile-not-found" } });
|
|
20
|
+
const res = await runAuthenticate("1234", { fetchImpl: f, uiPort: "1" });
|
|
21
|
+
expect(res).toBe("Authenticated as Ada.");
|
|
22
|
+
});
|
|
23
|
+
it("reports service-unavailable when the endpoint throws", async () => {
|
|
24
|
+
const f = vi.fn(async () => { throw new Error("ECONNREFUSED"); });
|
|
25
|
+
const res = await runAuthenticate("1234", { fetchImpl: f, uiPort: "1" });
|
|
26
|
+
expect(res).toContain("unavailable");
|
|
27
|
+
});
|
|
28
|
+
it("posts the pin to the loopback validate endpoint", async () => {
|
|
29
|
+
const f = vi.fn(async () => ({ json: async () => ({ kind: "miss" }) }));
|
|
30
|
+
await runAuthenticate("1234", { fetchImpl: f, uiPort: "19200" });
|
|
31
|
+
expect(f).toHaveBeenCalledWith("http://127.0.0.1:19200/api/admin/identity/validate", expect.objectContaining({ method: "POST", body: JSON.stringify({ pin: "1234" }) }));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=admin-identity-authenticate.test.js.map
|
package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-identity-authenticate.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-identity-authenticate.test.js","sourceRoot":"","sources":["../../src/__tests__/admin-identity-authenticate.test.ts"],"names":[],"mappings":"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"}
|
package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-no-prescribed-role.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/skill-no-prescribed-role.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Standing guard for Task 622 — no prescribed lead-capture role in any admin skill.
|
|
2
|
+
//
|
|
3
|
+
// SOUL.md is personality plus the role the operator defines at creation. The
|
|
4
|
+
// skill text must never re-prescribe a fixed Rubytech role (lead-capture /
|
|
5
|
+
// contact-form / "give basic public info"). This is a no-event failure mode:
|
|
6
|
+
// nothing logs when the prescribed role creeps back into the skill prose, so
|
|
7
|
+
// the reconciliation between expected state (no prescribed role) and actual
|
|
8
|
+
// file content lives here. A green run is the success signal; a match prints
|
|
9
|
+
// the offending `file:line` and fails the build.
|
|
10
|
+
import { describe, it, expect } from "vitest";
|
|
11
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "skills");
|
|
15
|
+
// Case-insensitive. The exact phrases the operator confirmed as prescriptive
|
|
16
|
+
// and wrong (session ebff1750); see .tasks/archive/622-*.md.
|
|
17
|
+
const FORBIDDEN = [
|
|
18
|
+
"capture the visitor's contact details",
|
|
19
|
+
"landing-page contact form",
|
|
20
|
+
"give basic public info",
|
|
21
|
+
];
|
|
22
|
+
function skillFiles(dir) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
25
|
+
const full = join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
out.push(...skillFiles(full));
|
|
28
|
+
else if (entry.name === "SKILL.md")
|
|
29
|
+
out.push(full);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
describe("admin skills carry no prescribed lead-capture role (Task 622)", () => {
|
|
34
|
+
it("no SKILL.md contains a forbidden prescribed-role phrase", () => {
|
|
35
|
+
const hits = [];
|
|
36
|
+
for (const file of skillFiles(SKILLS_DIR)) {
|
|
37
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
38
|
+
lines.forEach((line, i) => {
|
|
39
|
+
const lower = line.toLowerCase();
|
|
40
|
+
for (const phrase of FORBIDDEN) {
|
|
41
|
+
if (lower.includes(phrase.toLowerCase())) {
|
|
42
|
+
hits.push(`${file}:${i + 1} — ${phrase}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
expect(hits, `prescribed-role phrasing found:\n${hits.join("\n")}`).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=skill-no-prescribed-role.test.js.map
|
package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-no-prescribed-role.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-no-prescribed-role.test.js","sourceRoot":"","sources":["../../src/__tests__/skill-no-prescribed-role.test.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,iDAAiD;AACjD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAE7F,6EAA6E;AAC7E,6DAA6D;AAC7D,MAAM,SAAS,GAAG;IAChB,uCAAuC;IACvC,2BAA2B;IAC3B,wBAAwB;CACzB,CAAC;AAEF,SAAS,UAAU,CAAC,GAAW;IAC7B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;aAClD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,QAAQ,CAAC,+DAA+D,EAAE,GAAG,EAAE;IAC7E,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACrD,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;gBACxB,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjC,KAAK,MAAM,MAAM,IAAI,SAAS,EAAE,CAAC;oBAC/B,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;wBACzC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC;oBAC5C,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,oCAAoC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|