@rubytech/create-realagent 1.0.615 → 1.0.617
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/mcp-stderr-tee/dist/index.d.ts +23 -13
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +86 -89
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +86 -101
- package/payload/platform/package-lock.json +1547 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +33 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +22 -8
- package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +195 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +160 -214
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +203 -42
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +623 -195
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/mcp/vitest.config.ts +10 -0
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +26 -30
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +28 -4
- package/payload/platform/plugins/docs/PLUGIN.md +2 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +51 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
- package/payload/platform/scripts/logs-read.sh +114 -54
- package/payload/platform/templates/specialists/agents/personal-assistant.md +12 -8
- package/payload/server/server.js +387 -70
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloudflare
|
|
3
|
-
description: Cloudflare Tunnel setup and management
|
|
3
|
+
description: Cloudflare Tunnel setup and management — bound account is the universe, agent owns it absolutely
|
|
4
4
|
tools:
|
|
5
5
|
- cloudflare-setup
|
|
6
|
-
- cf-set-token
|
|
7
6
|
- cf-add-zone
|
|
8
|
-
- tunnel-login
|
|
9
7
|
- cf-zone-status
|
|
8
|
+
- cf-verify
|
|
9
|
+
- cf-rebuild
|
|
10
|
+
- tunnel-login
|
|
10
11
|
- tunnel-status
|
|
11
12
|
- tunnel-install
|
|
12
13
|
- tunnel-create
|
|
@@ -18,7 +19,7 @@ tools:
|
|
|
18
19
|
|
|
19
20
|
# Cloudflare Tunnel Setup
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Each installation has its own Cloudflare account, owned by the user, accessed via `cert.pem` from `tunnel-login` (OAuth — the only auth path). The bound account is the entire universe of routable zones; the agent has absolute authority over it (add, delete, restructure). Anything on the account that doesn't belong to the user's current intended state is junk — `cf-rebuild` deletes it. `cloudflare-setup` is the onboarding orchestrator: a UI-driven state machine that surfaces what's on the account, asks the user to pick a domain and subdomains, then creates the tunnel. `cf-verify` is the non-mutating audit (account state + device state + orphans). The device records `account-binding.json` on first login so cert rotation under a different account is detected and refused — that's the only inherited identity check.
|
|
22
23
|
|
|
23
24
|
## When to activate
|
|
24
25
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
CloudflareRefusalError,
|
|
7
|
+
_resetBrandCache,
|
|
8
|
+
getClient,
|
|
9
|
+
getReadOnlyClient,
|
|
10
|
+
readAccountBinding,
|
|
11
|
+
validateAuth,
|
|
12
|
+
writeAccountBinding,
|
|
13
|
+
} from "../src/lib/cloudflared.js";
|
|
14
|
+
|
|
15
|
+
// Tests for the auth pre-conditions that getClient() enforces. We don't mock
|
|
16
|
+
// the Cloudflare SDK here — we only ever exercise the gate, not the SDK call.
|
|
17
|
+
// The gate throws CloudflareRefusalError BEFORE constructing the client, so
|
|
18
|
+
// no network is touched.
|
|
19
|
+
|
|
20
|
+
let tmpRoot: string;
|
|
21
|
+
let homeRoot: string;
|
|
22
|
+
let originalHome: string | undefined;
|
|
23
|
+
|
|
24
|
+
function writeBrand(content: object): void {
|
|
25
|
+
const dir = join(tmpRoot, "config");
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
writeFileSync(join(dir, "brand.json"), JSON.stringify(content));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeCertPem(accountId: string, apiToken: string): void {
|
|
31
|
+
const certDir = join(homeRoot, ".maxy", "cloudflared");
|
|
32
|
+
mkdirSync(certDir, { recursive: true });
|
|
33
|
+
const tokenJson = JSON.stringify({ APIToken: apiToken, AccountTag: accountId });
|
|
34
|
+
const b64 = Buffer.from(tokenJson, "utf-8").toString("base64");
|
|
35
|
+
const pem = `dummy-private-key
|
|
36
|
+
-----BEGIN ARGO TUNNEL TOKEN-----
|
|
37
|
+
${b64}
|
|
38
|
+
-----END ARGO TUNNEL TOKEN-----
|
|
39
|
+
`;
|
|
40
|
+
writeFileSync(join(certDir, "cert.pem"), pem);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "maxy-cf-auth-"));
|
|
45
|
+
homeRoot = mkdtempSync(join(tmpdir(), "maxy-cf-home-"));
|
|
46
|
+
originalHome = process.env.HOME;
|
|
47
|
+
process.env.HOME = homeRoot;
|
|
48
|
+
process.env.PLATFORM_ROOT = tmpRoot;
|
|
49
|
+
_resetBrandCache();
|
|
50
|
+
writeBrand({
|
|
51
|
+
productName: "Maxy",
|
|
52
|
+
configDir: ".maxy",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
58
|
+
else delete process.env.HOME;
|
|
59
|
+
delete process.env.PLATFORM_ROOT;
|
|
60
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
61
|
+
rmSync(homeRoot, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("validateAuth — pure read of cert + binding state", () => {
|
|
65
|
+
it("reports nothing on a fresh install", () => {
|
|
66
|
+
const auth = validateAuth();
|
|
67
|
+
expect(auth.hasCert).toBe(false);
|
|
68
|
+
expect(auth.hasBinding).toBe(false);
|
|
69
|
+
expect(auth.bound).toBe(false);
|
|
70
|
+
expect(auth.certAccountId).toBeNull();
|
|
71
|
+
expect(auth.boundAccountId).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reports cert when present, no binding", () => {
|
|
75
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
76
|
+
const auth = validateAuth();
|
|
77
|
+
expect(auth.hasCert).toBe(true);
|
|
78
|
+
expect(auth.hasBinding).toBe(false);
|
|
79
|
+
expect(auth.bound).toBe(false);
|
|
80
|
+
expect(auth.certAccountId).toBe("acct-X");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reports bound when cert + binding agree", () => {
|
|
84
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
85
|
+
writeAccountBinding("acct-X");
|
|
86
|
+
const auth = validateAuth();
|
|
87
|
+
expect(auth.bound).toBe(true);
|
|
88
|
+
expect(auth.certAccountId).toBe("acct-X");
|
|
89
|
+
expect(auth.boundAccountId).toBe("acct-X");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reports drift when cert and binding disagree", () => {
|
|
93
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
94
|
+
writeAccountBinding("acct-Y");
|
|
95
|
+
const auth = validateAuth();
|
|
96
|
+
expect(auth.hasCert).toBe(true);
|
|
97
|
+
expect(auth.hasBinding).toBe(true);
|
|
98
|
+
expect(auth.bound).toBe(false);
|
|
99
|
+
expect(auth.certAccountId).toBe("acct-X");
|
|
100
|
+
expect(auth.boundAccountId).toBe("acct-Y");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("getClient — refuses uncircumventably on auth failures", () => {
|
|
105
|
+
it("refuses unbound-device when cert is absent", () => {
|
|
106
|
+
expect(() => getClient()).toThrow(CloudflareRefusalError);
|
|
107
|
+
try {
|
|
108
|
+
getClient();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
expect((err as CloudflareRefusalError).refusal.reason).toBe("unbound-device");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("refuses unbound-device when cert exists but no binding", () => {
|
|
115
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
116
|
+
try {
|
|
117
|
+
getClient();
|
|
118
|
+
throw new Error("expected refusal");
|
|
119
|
+
} catch (err) {
|
|
120
|
+
expect(err).toBeInstanceOf(CloudflareRefusalError);
|
|
121
|
+
expect((err as CloudflareRefusalError).refusal.reason).toBe("unbound-device");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("refuses account-drift when cert and binding disagree", () => {
|
|
126
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
127
|
+
writeAccountBinding("acct-Y");
|
|
128
|
+
try {
|
|
129
|
+
getClient();
|
|
130
|
+
throw new Error("expected refusal");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
expect(err).toBeInstanceOf(CloudflareRefusalError);
|
|
133
|
+
const refusal = (err as CloudflareRefusalError).refusal;
|
|
134
|
+
expect(refusal.reason).toBe("account-drift");
|
|
135
|
+
expect(refusal.fields.certAccountId).toBe("acct-X");
|
|
136
|
+
expect(refusal.fields.boundAccountId).toBe("acct-Y");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("succeeds when cert + binding agree", () => {
|
|
141
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
142
|
+
writeAccountBinding("acct-X");
|
|
143
|
+
const { accountId } = getClient();
|
|
144
|
+
expect(accountId).toBe("acct-X");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("getReadOnlyClient — never throws, surfaces partial state", () => {
|
|
149
|
+
it("returns nulls on fresh install", () => {
|
|
150
|
+
const ro = getReadOnlyClient();
|
|
151
|
+
expect(ro.client).toBeNull();
|
|
152
|
+
expect(ro.certAccountId).toBeNull();
|
|
153
|
+
expect(ro.boundAccountId).toBeNull();
|
|
154
|
+
expect(ro.bindingMatches).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns cert info when cert exists, no binding", () => {
|
|
158
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
159
|
+
const ro = getReadOnlyClient();
|
|
160
|
+
expect(ro.client).not.toBeNull();
|
|
161
|
+
expect(ro.certAccountId).toBe("acct-X");
|
|
162
|
+
expect(ro.boundAccountId).toBeNull();
|
|
163
|
+
expect(ro.bindingMatches).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("reports bindingMatches when cert + binding agree", () => {
|
|
167
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
168
|
+
writeAccountBinding("acct-X");
|
|
169
|
+
const ro = getReadOnlyClient();
|
|
170
|
+
expect(ro.bindingMatches).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("reports bindingMatches=false on drift but still returns client + ids", () => {
|
|
174
|
+
writeCertPem("acct-X", "cfut_dummy");
|
|
175
|
+
writeAccountBinding("acct-Y");
|
|
176
|
+
const ro = getReadOnlyClient();
|
|
177
|
+
expect(ro.client).not.toBeNull();
|
|
178
|
+
expect(ro.certAccountId).toBe("acct-X");
|
|
179
|
+
expect(ro.boundAccountId).toBe("acct-Y");
|
|
180
|
+
expect(ro.bindingMatches).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("account-binding write/read round-trip", () => {
|
|
185
|
+
it("persists accountId and a parseable boundAt", () => {
|
|
186
|
+
writeAccountBinding("acct-Z");
|
|
187
|
+
const binding = readAccountBinding();
|
|
188
|
+
expect(binding?.accountId).toBe("acct-Z");
|
|
189
|
+
expect(binding?.boundAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns null when no binding file exists", () => {
|
|
193
|
+
expect(readAccountBinding()).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
});
|