@rubytech/create-maxy-code 0.1.451 → 0.1.453
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/dist/__tests__/preserve-house-cloudflare.test.js +81 -2
- package/dist/index.js +10 -1
- package/dist/preserve-house-cloudflare.js +49 -2
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +24 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.js +48 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-scoped-token.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js +6 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js +14 -4
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +18 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.d.ts +9 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js +60 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/remediate.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/remediate.js +14 -1
- package/payload/platform/lib/storage-broker/dist/remediate.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +28 -1
- package/payload/platform/lib/storage-broker/src/__tests__/house-scoped-token.test.ts +48 -0
- package/payload/platform/lib/storage-broker/src/__tests__/remediate.test.ts +8 -0
- package/payload/platform/lib/storage-broker/src/bin/remediate-run.ts +17 -5
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +22 -0
- package/payload/platform/lib/storage-broker/src/house-scoped-token.ts +72 -0
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/remediate.ts +14 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-gate.test.sh +107 -0
- package/payload/platform/plugins/admin/hooks/quote-render-gate.sh +74 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -4
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-token.test.sh +25 -1
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +9 -1
- package/payload/platform/plugins/cloudflare/references/api.md +3 -1
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/outlook-guide.md +3 -3
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.js +131 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/complete-registration.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js +55 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js +120 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +66 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.js +48 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-scan.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.d.ts +31 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.js +47 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/complete-registration.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.d.ts +11 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.js +31 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pending-scan.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +13 -9
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +19 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +14 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.js +111 -0
- package/payload/platform/plugins/outlook/mcp/dist/scripts/complete-registration.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +16 -1
- 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 +37 -7
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts +12 -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 +16 -12
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +2 -0
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +20 -9
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +7 -7
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.js +36 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/house-scoped-token.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.d.ts +9 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.js +51 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/house-scoped-token.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +16 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +16 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/scripts/lib/provision-account-dir.sh +6 -0
- package/payload/platform/scripts/smoke-boot-services.sh +1 -0
- package/payload/platform/scripts/voice-mirror-audit.sh +70 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +7 -1
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +15 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +38 -3
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +21 -10
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +20 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.d.ts +34 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.js +66 -0
- package/payload/platform/services/claude-session-manager/dist/plugin-enum.js.map +1 -0
- 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 +8 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/server/{chunk-TTKYHVA7.js → chunk-SDYRKIYY.js} +70 -3
- package/payload/server/server.js +59 -10
- package/payload/server/{src-FBUXV2FV.js → src-554BYJMN.js} +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,4DAA4D;;;;;;;;;;;;;;;;AAE5D,iDAA+B;AAC/B,gDAA8B;AAC9B,wDAAsC;AACtC,+CAA6B;AAC7B,6CAA2B;AAC3B,iDAA+B"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,4DAA4D;;;;;;;;;;;;;;;;AAE5D,iDAA+B;AAC/B,gDAA8B;AAC9B,wDAAsC;AACtC,+CAA6B;AAC7B,0DAAwC;AACxC,6CAA2B;AAC3B,iDAA+B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remediate.d.ts","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"remediate.d.ts","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,aAAa,CAGf;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM/D"}
|
|
@@ -10,7 +10,20 @@ exports.stripAccountWideTokens = stripAccountWideTokens;
|
|
|
10
10
|
// Account-wide credentials stripped from a sub-account's cloudflare.env.
|
|
11
11
|
// CLOUDFLARE_ACCOUNT_ID is intentionally kept: it is an identifier, not a
|
|
12
12
|
// credential, and does not grant access on its own.
|
|
13
|
-
|
|
13
|
+
// Account-wide-reach tokens that must never persist in a sub-account secrets
|
|
14
|
+
// file: the master and every scoped DATA token that reaches the whole shared
|
|
15
|
+
// account. CF_STORAGE_TOKEN (D1+R2) and CF_CALENDAR_D1_TOKEN (D1) are house-code
|
|
16
|
+
// data scopes (Task 1670) minted account-scoped, so a copy in a sub-account file
|
|
17
|
+
// would let that tenant reach every other tenant's D1/R2 — strip them too. The
|
|
18
|
+
// account's own zone/pages hosting tokens (CF_DNS_TOKEN, etc.) are narrow and
|
|
19
|
+
// stay.
|
|
20
|
+
const STRIP_KEYS = [
|
|
21
|
+
"CLOUDFLARE_API_TOKEN",
|
|
22
|
+
"CF_PAGES_D1_TOKEN",
|
|
23
|
+
"CF_PAGES_TOKEN",
|
|
24
|
+
"CF_STORAGE_TOKEN",
|
|
25
|
+
"CF_CALENDAR_D1_TOKEN",
|
|
26
|
+
];
|
|
14
27
|
function validateMapping(enumerated, mapping) {
|
|
15
28
|
const missing = enumerated.filter((name) => !(name in mapping));
|
|
16
29
|
return { ok: missing.length === 0, missing };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remediate.js","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,wEAAwE;;
|
|
1
|
+
{"version":3,"file":"remediate.js","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,wEAAwE;;AAyBxE,0CAMC;AAED,wDAMC;AAhCD,yEAAyE;AACzE,0EAA0E;AAC1E,oDAAoD;AACpD,6EAA6E;AAC7E,6EAA6E;AAC7E,iFAAiF;AACjF,iFAAiF;AACjF,+EAA+E;AAC/E,8EAA8E;AAC9E,QAAQ;AACR,MAAM,UAAU,GAAG;IACjB,sBAAsB;IACtB,mBAAmB;IACnB,gBAAgB;IAChB,kBAAkB;IAClB,sBAAsB;CACvB,CAAC;AAEF,SAAgB,eAAe,CAC7B,UAAoB,EACpB,OAA+B;IAE/B,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC;IAChE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;AAC/C,CAAC;AAED,SAAgB,sBAAsB,CAAC,QAAgB;IACrD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7D,OAAO,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { makeCfExec, makeHouseCfExec, type RunFn, type FetchFn } from "../cf-exec.js";
|
|
4
7
|
|
|
5
8
|
const cred = { apiToken: "cfat_x", accountId: "acc-h" };
|
|
6
9
|
|
|
@@ -158,3 +161,27 @@ test("non-JSON output throws with the raw body", async () => {
|
|
|
158
161
|
const cf = makeCfExec(cred, run);
|
|
159
162
|
await assert.rejects(() => cf.d1List(), /not json/);
|
|
160
163
|
});
|
|
164
|
+
|
|
165
|
+
// Task 1670 — makeHouseCfExec resolves a scoped storage token from the house
|
|
166
|
+
// minter via cf-token.sh and uses THAT token (never the minter) for wrangler.
|
|
167
|
+
test("makeHouseCfExec runs wrangler with the scoped token, not the minter", async () => {
|
|
168
|
+
const platformRoot = mkdtempSync(join(tmpdir(), "house-cfexec-"));
|
|
169
|
+
const cfg = join(platformRoot, "config");
|
|
170
|
+
mkdirSync(cfg, { recursive: true });
|
|
171
|
+
const houseEnv = join(cfg, "cloudflare-house.env");
|
|
172
|
+
writeFileSync(houseEnv, "CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc-h\n");
|
|
173
|
+
|
|
174
|
+
let sawToken = "";
|
|
175
|
+
const run: RunFn = async (_cmd, args, env) => {
|
|
176
|
+
if (args.some((a) => a.endsWith("cf-token.sh"))) {
|
|
177
|
+
appendFileSync(houseEnv, "CF_STORAGE_TOKEN=scoped_tok\n");
|
|
178
|
+
return { stdout: "CF_STORAGE_TOKEN\n" };
|
|
179
|
+
}
|
|
180
|
+
sawToken = env.CLOUDFLARE_API_TOKEN;
|
|
181
|
+
return { stdout: "[]" };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const cf = await makeHouseCfExec(platformRoot, run);
|
|
185
|
+
await cf.d1List();
|
|
186
|
+
assert.equal(sawToken, "scoped_tok", "wrangler saw the scoped token, not the minter");
|
|
187
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Task 1670 — resolveHouseScopedToken shells cf-token.sh against the house env
|
|
2
|
+
// file and reads back the persisted key value. The cf-token.sh invocation is
|
|
3
|
+
// mocked via the injected RunFn (it appends the key and echoes the key name).
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, writeFileSync, appendFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { resolveHouseScopedToken } from "../house-scoped-token.js";
|
|
10
|
+
import type { RunFn } from "../cf-exec.js";
|
|
11
|
+
|
|
12
|
+
function houseFile(contents: string): string {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "hst-"));
|
|
14
|
+
const p = join(dir, "cloudflare-house.env");
|
|
15
|
+
writeFileSync(p, contents);
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("mints and reads back the scoped token value", async () => {
|
|
20
|
+
const p = houseFile("CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc\n");
|
|
21
|
+
const run: RunFn = async (_cmd, args) => {
|
|
22
|
+
assert.equal(args[1], "storage", "scope passed to cf-token.sh");
|
|
23
|
+
appendFileSync(p, "CF_STORAGE_TOKEN=minted_value\n");
|
|
24
|
+
return { stdout: "CF_STORAGE_TOKEN\n" };
|
|
25
|
+
};
|
|
26
|
+
const tok = await resolveHouseScopedToken("storage", p, "/x/cf-token.sh", run);
|
|
27
|
+
assert.equal(tok, "minted_value");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("reuse: a pre-existing key resolves to its value", async () => {
|
|
31
|
+
const p = houseFile("CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc\nCF_STORAGE_TOKEN=already_here\n");
|
|
32
|
+
// cf-token.sh reuse path writes nothing new, still echoes the key.
|
|
33
|
+
const run: RunFn = async () => ({ stdout: "CF_STORAGE_TOKEN\n" });
|
|
34
|
+
const tok = await resolveHouseScopedToken("storage", p, "/x/cf-token.sh", run);
|
|
35
|
+
assert.equal(tok, "already_here");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("throws when the persisted key is absent after the run", async () => {
|
|
39
|
+
const p = houseFile("CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc\n");
|
|
40
|
+
const run: RunFn = async () => ({ stdout: "CF_STORAGE_TOKEN\n" }); // writes nothing
|
|
41
|
+
await assert.rejects(() => resolveHouseScopedToken("storage", p, "/x/cf-token.sh", run));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("throws when cf-token.sh returns no key name", async () => {
|
|
45
|
+
const p = houseFile("CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc\n");
|
|
46
|
+
const run: RunFn = async () => ({ stdout: "\n" });
|
|
47
|
+
await assert.rejects(() => resolveHouseScopedToken("storage", p, "/x/cf-token.sh", run));
|
|
48
|
+
});
|
|
@@ -19,3 +19,11 @@ test("strips only account-wide token lines, keeps the account id and scoped toke
|
|
|
19
19
|
"CLOUDFLARE_ACCOUNT_ID=acc\nCLOUDFLARE_API_TOKEN=cfat_x\nCF_PAGES_D1_TOKEN=y\nCF_DNS_TOKEN=keep\n";
|
|
20
20
|
assert.equal(stripAccountWideTokens(before), "CLOUDFLARE_ACCOUNT_ID=acc\nCF_DNS_TOKEN=keep\n");
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
// Task 1670 — the house-code data scopes reach the whole shared account, so a
|
|
24
|
+
// stray copy in a sub-account file is a cross-tenant leak and must be stripped.
|
|
25
|
+
test("strips the account-wide data scopes (storage, calendar-d1), keeps zone hosting tokens", () => {
|
|
26
|
+
const before =
|
|
27
|
+
"CLOUDFLARE_ACCOUNT_ID=acc\nCF_STORAGE_TOKEN=cfat_d1r2\nCF_CALENDAR_D1_TOKEN=cfat_d1\nCF_DNS_TOKEN=keep\n";
|
|
28
|
+
assert.equal(stripAccountWideTokens(before), "CLOUDFLARE_ACCOUNT_ID=acc\nCF_DNS_TOKEN=keep\n");
|
|
29
|
+
});
|
|
@@ -14,6 +14,7 @@ import { validateMapping, stripAccountWideTokens } from "../remediate.js";
|
|
|
14
14
|
import { registerResource, type StorageResource } from "../registry.js";
|
|
15
15
|
import { readHouseCredential } from "../house-credential.js";
|
|
16
16
|
import { makeCfExec } from "../cf-exec.js";
|
|
17
|
+
import { resolveHouseScopedToken } from "../house-scoped-token.js";
|
|
17
18
|
|
|
18
19
|
export interface RemediationDeps {
|
|
19
20
|
cfD1(): Promise<Array<{ name: string; cfResourceId: string }>>;
|
|
@@ -77,7 +78,9 @@ export async function runRemediation(deps: RemediationDeps): Promise<Remediation
|
|
|
77
78
|
|
|
78
79
|
// --- on-device CLI (untested glue) ----------------------------------------
|
|
79
80
|
|
|
80
|
-
function findMasterFromAccounts(
|
|
81
|
+
function findMasterFromAccounts(
|
|
82
|
+
accountsDir: string,
|
|
83
|
+
): { apiToken: string; accountId: string; path: string } | null {
|
|
81
84
|
if (!existsSync(accountsDir)) return null;
|
|
82
85
|
for (const accountId of readdirSync(accountsDir)) {
|
|
83
86
|
const p = join(accountsDir, accountId, "secrets", "cloudflare.env");
|
|
@@ -88,7 +91,7 @@ function findMasterFromAccounts(accountsDir: string): { apiToken: string; accoun
|
|
|
88
91
|
if (m) env[m[1]] = m[2].trim();
|
|
89
92
|
}
|
|
90
93
|
if (env.CLOUDFLARE_API_TOKEN && env.CLOUDFLARE_ACCOUNT_ID) {
|
|
91
|
-
return { apiToken: env.CLOUDFLARE_API_TOKEN, accountId: env.CLOUDFLARE_ACCOUNT_ID };
|
|
94
|
+
return { apiToken: env.CLOUDFLARE_API_TOKEN, accountId: env.CLOUDFLARE_ACCOUNT_ID, path: p };
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
return null;
|
|
@@ -110,10 +113,13 @@ async function main(): Promise<void> {
|
|
|
110
113
|
const mapping = JSON.parse(readFileSync(mappingFile, "utf8")) as Record<string, string>;
|
|
111
114
|
|
|
112
115
|
// The master is wherever it lives today: the house file if already relocated,
|
|
113
|
-
// else the first sub-account secrets file that still carries it.
|
|
116
|
+
// else the first sub-account secrets file that still carries it. masterFile is
|
|
117
|
+
// the file cf-token.sh mints the scoped enumeration token against.
|
|
114
118
|
let master: { apiToken: string; accountId: string };
|
|
119
|
+
let masterFile: string;
|
|
115
120
|
try {
|
|
116
121
|
master = readHouseCredential(platformRoot);
|
|
122
|
+
masterFile = configPath;
|
|
117
123
|
} catch {
|
|
118
124
|
const found = findMasterFromAccounts(accountsDir);
|
|
119
125
|
if (!found) {
|
|
@@ -121,10 +127,16 @@ async function main(): Promise<void> {
|
|
|
121
127
|
process.exit(2);
|
|
122
128
|
return;
|
|
123
129
|
}
|
|
124
|
-
master = found;
|
|
130
|
+
master = { apiToken: found.apiToken, accountId: found.accountId };
|
|
131
|
+
masterFile = found.path;
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
134
|
+
// Task 1670 — the master is a minter; enumerate D1/R2 through a scoped storage
|
|
135
|
+
// token minted from it, never the minter directly. `master` (the raw minter)
|
|
136
|
+
// is still what runRemediation relocates into the house file below.
|
|
137
|
+
const cfTokenSh = join(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
|
|
138
|
+
const scoped = await resolveHouseScopedToken("storage", masterFile, cfTokenSh);
|
|
139
|
+
const cf = makeCfExec({ apiToken: scoped, accountId: master.accountId });
|
|
128
140
|
const neo4j = (await import("neo4j-driver")) as any;
|
|
129
141
|
const driver = neo4j.default.driver(
|
|
130
142
|
process.env.NEO4J_URI,
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// wrangler. `run` is injectable so the wiring is unit-testable without wrangler.
|
|
4
4
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
|
+
import { join } from "node:path";
|
|
6
7
|
import type { HouseCredential } from "./house-credential.js";
|
|
8
|
+
import { readHouseCredential } from "./house-credential.js";
|
|
9
|
+
import { resolveHouseScopedToken } from "./house-scoped-token.js";
|
|
7
10
|
|
|
8
11
|
export type RunFn = (
|
|
9
12
|
cmd: string,
|
|
@@ -137,3 +140,22 @@ export function makeCfExec(
|
|
|
137
140
|
},
|
|
138
141
|
};
|
|
139
142
|
}
|
|
143
|
+
|
|
144
|
+
// Task 1670 — build a CfExec whose token is a D1+R2 "storage" scope minted from
|
|
145
|
+
// the house minter, never the minter used directly (a minter cannot run
|
|
146
|
+
// wrangler d1/r2 or hit the D1/R2 API — it returns 10000). cf-token.sh
|
|
147
|
+
// mints-or-reuses and persists the scope house-side; the resolved value is used
|
|
148
|
+
// for both the wrangler run env and the API Bearer. The two UI-route broker
|
|
149
|
+
// sites and remediate-run construct their CfExec through the house minter this
|
|
150
|
+
// way instead of `makeCfExec(readHouseCredential(...))`.
|
|
151
|
+
export async function makeHouseCfExec(
|
|
152
|
+
platformRoot: string,
|
|
153
|
+
run: RunFn = defaultRun,
|
|
154
|
+
fetchFn: FetchFn = defaultFetch,
|
|
155
|
+
): Promise<CfExec> {
|
|
156
|
+
const cred = readHouseCredential(platformRoot);
|
|
157
|
+
const houseEnvPath = join(platformRoot, "config", "cloudflare-house.env");
|
|
158
|
+
const cfTokenSh = join(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
|
|
159
|
+
const scoped = await resolveHouseScopedToken("storage", houseEnvPath, cfTokenSh, run);
|
|
160
|
+
return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
|
|
161
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Task 1670 — the house credential is a minter. Each broker/calendar consumer
|
|
2
|
+
// mints (and thereafter reuses) a scoped data token from it via cf-token.sh, the
|
|
3
|
+
// single Cloudflare mint/reuse state machine. This wrapper shells that script
|
|
4
|
+
// against the house env file and reads back the persisted key value. The mint
|
|
5
|
+
// logic itself is not duplicated here — it lives only in cf-token.sh.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import type { RunFn } from "./cf-exec.js";
|
|
10
|
+
|
|
11
|
+
const defaultRun: RunFn = (cmd, args, env) =>
|
|
12
|
+
new Promise((resolve, reject) => {
|
|
13
|
+
execFile(cmd, args, { env }, (err, stdout, stderr) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}\n${stderr}`));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
resolve({ stdout: stdout.toString() });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function readKey(path: string, key: string): string | undefined {
|
|
23
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
24
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
25
|
+
if (m && m[1] === key) return m[2].trim();
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a scoped data token (e.g. "storage" = D1+R2) from the house minter.
|
|
32
|
+
* Shells cf-token.sh, which mints or reuses the scope and persists the key into
|
|
33
|
+
* the house env file, echoing the key name. Reads that key back and returns its
|
|
34
|
+
* value. Throws if the key is absent after the run (never returns the minter).
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveHouseScopedToken(
|
|
37
|
+
scope: string,
|
|
38
|
+
houseEnvPath: string,
|
|
39
|
+
cfTokenShPath: string,
|
|
40
|
+
run: RunFn = defaultRun,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const preexisting = readKey(houseEnvPath, keyForScope(scope)) !== undefined;
|
|
43
|
+
const { stdout } = await run(
|
|
44
|
+
"bash",
|
|
45
|
+
[cfTokenShPath, scope, houseEnvPath],
|
|
46
|
+
{ ...process.env } as Record<string, string>,
|
|
47
|
+
);
|
|
48
|
+
const keyName = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
49
|
+
if (!keyName) throw new Error(`storage-broker: cf-token.sh ${scope} returned no key name`);
|
|
50
|
+
const value = readKey(houseEnvPath, keyName);
|
|
51
|
+
if (!value) {
|
|
52
|
+
throw new Error(`storage-broker: scoped token ${keyName} absent in ${houseEnvPath} after mint`);
|
|
53
|
+
}
|
|
54
|
+
console.error(
|
|
55
|
+
`[storage-broker] op=house-scoped-token src=house scope=${scope} action=${preexisting ? "reuse" : "mint"} result=active`,
|
|
56
|
+
);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The persisted key name for each scope, mirroring cf-token.sh's scope table.
|
|
61
|
+
// Used only to derive the mint/reuse action for the observability line; the
|
|
62
|
+
// authoritative key is whatever cf-token.sh echoes.
|
|
63
|
+
function keyForScope(scope: string): string {
|
|
64
|
+
switch (scope) {
|
|
65
|
+
case "storage":
|
|
66
|
+
return "CF_STORAGE_TOKEN";
|
|
67
|
+
case "calendar-d1":
|
|
68
|
+
return "CF_CALENDAR_D1_TOKEN";
|
|
69
|
+
default:
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -12,7 +12,20 @@ export interface MappingResult {
|
|
|
12
12
|
// Account-wide credentials stripped from a sub-account's cloudflare.env.
|
|
13
13
|
// CLOUDFLARE_ACCOUNT_ID is intentionally kept: it is an identifier, not a
|
|
14
14
|
// credential, and does not grant access on its own.
|
|
15
|
-
|
|
15
|
+
// Account-wide-reach tokens that must never persist in a sub-account secrets
|
|
16
|
+
// file: the master and every scoped DATA token that reaches the whole shared
|
|
17
|
+
// account. CF_STORAGE_TOKEN (D1+R2) and CF_CALENDAR_D1_TOKEN (D1) are house-code
|
|
18
|
+
// data scopes (Task 1670) minted account-scoped, so a copy in a sub-account file
|
|
19
|
+
// would let that tenant reach every other tenant's D1/R2 — strip them too. The
|
|
20
|
+
// account's own zone/pages hosting tokens (CF_DNS_TOKEN, etc.) are narrow and
|
|
21
|
+
// stay.
|
|
22
|
+
const STRIP_KEYS = [
|
|
23
|
+
"CLOUDFLARE_API_TOKEN",
|
|
24
|
+
"CF_PAGES_D1_TOKEN",
|
|
25
|
+
"CF_PAGES_TOKEN",
|
|
26
|
+
"CF_STORAGE_TOKEN",
|
|
27
|
+
"CF_CALENDAR_D1_TOKEN",
|
|
28
|
+
];
|
|
16
29
|
|
|
17
30
|
export function validateMapping(
|
|
18
31
|
enumerated: string[],
|
|
@@ -180,6 +180,7 @@ Tools are available via the `admin` MCP server.
|
|
|
180
180
|
|
|
181
181
|
## Hooks
|
|
182
182
|
|
|
183
|
+
- `hooks/quote-render-gate.sh` — **PreToolUse matcher=`mcp__plugin_browser_browser__browser-pdf-save`.** Makes the deterministic quote-render path the only route a client-facing quote PDF can reach a customer. Blocks (exit 2) a `browser-pdf-save` whose output path is a client-quote artefact (`memory/users/<phone>/documents/quote-<jobId>.pdf` or `…-breakdown.pdf`) when no render receipt exists at `quoting/jobs/<jobId>/render-receipt.json` — the receipt `quote-render` writes only after its reconcile / margin-leak / link-liveness gates all pass. A hand-authored quote (the SiteDesk 2026-07-14 bypass: `Write` HTML → `browser-pdf-save`, `quote-render` never called, an internal back-test caveat naming another client folded into the free-text region) has no receipt, so the print is refused rather than delivered. **Block message:** `Blocked: this client quote was not produced by the quote-render tool, so its fixed layout and the reconcile, margin-leak and link-liveness gates never ran. Run quote-render for this job (it files the document and its receipt), then print the client HTML path it returns to PDF.` **Log line** (stderr, on a block): `[quote-render] op=bypass jobId=<jobId> detail=client quote PDF has no render receipt` (or `reason=bad-jobid` when the filename does not resolve to a job). Every non-quote PDF — brochures, invoices, the admin-scope internal view — and the deterministic path itself (receipt present) pass at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3`): `browser-pdf-save` is a general-purpose tool and the agent never controls this hook's stdin, so failing open opens no bypass while never bricking unrelated PDFs.
|
|
183
184
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
184
185
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
185
186
|
- `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your channel reply tool and wait for the operator's next message.` **Log line** (stderr, one per decisive call): `[ask-channel] decision=<allow|block> sessionId=<id8> channel=<attached|none> reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>`. **Fail-open** on no envelope, blank session id, or absent channel file — nudges, never bricks the UI. The complementary standing check (`interactive-wedge` in the session manager) catches any un-carried interactive tool_use that slips past this hook.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for quote-render-gate.sh.
|
|
3
|
+
#
|
|
4
|
+
# The gate is a PreToolUse hook on browser-pdf-save. It refuses a client-facing
|
|
5
|
+
# quote PDF (memory/users/<phone>/documents/quote-<jobId>.pdf or
|
|
6
|
+
# ...-breakdown.pdf) unless a render receipt exists for that jobId — proof the
|
|
7
|
+
# deterministic quote-render path produced it. Everything else passes.
|
|
8
|
+
#
|
|
9
|
+
# Covers:
|
|
10
|
+
# A. client quote PDF, no receipt → BLOCKED (exit 2)
|
|
11
|
+
# B. client quote PDF, receipt present → ALLOWED (exit 0)
|
|
12
|
+
# C. client breakdown PDF, no receipt → BLOCKED
|
|
13
|
+
# D. client breakdown PDF, receipt present → ALLOWED
|
|
14
|
+
# E. non-quote PDF in the documents scope → ALLOWED (not a quote artefact)
|
|
15
|
+
# F. internal quote PDF (admin scope) → ALLOWED (not client-facing)
|
|
16
|
+
# G. quote-named PDF outside the client scope → ALLOWED (only the documents scope is gated)
|
|
17
|
+
# H. empty stdin → ALLOWED (general-purpose tool, fail-open)
|
|
18
|
+
# I. a non-browser tool → ALLOWED (matcher defence-in-depth)
|
|
19
|
+
# J. jobId carrying a path traversal → BLOCKED (never resolves a stray receipt)
|
|
20
|
+
# K. a block emits the op=bypass fault line
|
|
21
|
+
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/quote-render-gate.sh"
|
|
25
|
+
if [[ ! -x "$HOOK" ]]; then
|
|
26
|
+
echo "FAIL: $HOOK not executable" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
ACCT=$(mktemp -d)
|
|
31
|
+
cleanup() { rm -rf "$ACCT"; }
|
|
32
|
+
trap cleanup EXIT
|
|
33
|
+
|
|
34
|
+
# A receipt for job-1 (present) — jobs job-2 / job-3 have none.
|
|
35
|
+
mkdir -p "$ACCT/quoting/jobs/job-1"
|
|
36
|
+
printf '{"jobId":"job-1"}' > "$ACCT/quoting/jobs/job-1/render-receipt.json"
|
|
37
|
+
|
|
38
|
+
DOCS="$ACCT/memory/users/44700900000/documents"
|
|
39
|
+
ADMIN="$ACCT/memory/admin/customers/44700900000"
|
|
40
|
+
|
|
41
|
+
PASS=0
|
|
42
|
+
FAIL=0
|
|
43
|
+
|
|
44
|
+
# Run the hook with cwd = the account dir (as the PTY does), capturing exit + stderr.
|
|
45
|
+
run_case() {
|
|
46
|
+
local name="$1" stdin="$2" expected_exit="$3"
|
|
47
|
+
local actual_exit
|
|
48
|
+
( cd "$ACCT" && printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>/dev/null )
|
|
49
|
+
actual_exit=$?
|
|
50
|
+
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
|
51
|
+
echo "PASS: $name (exit=$actual_exit)"
|
|
52
|
+
PASS=$((PASS + 1))
|
|
53
|
+
else
|
|
54
|
+
echo "FAIL: $name (expected exit=$expected_exit, got=$actual_exit)" >&2
|
|
55
|
+
FAIL=$((FAIL + 1))
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
envelope() { # tool_name, path
|
|
60
|
+
printf '{"hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"path":"%s"}}' "$1" "$2"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
BROWSER="mcp__plugin_browser_browser__browser-pdf-save"
|
|
64
|
+
|
|
65
|
+
run_case "A: client quote PDF, no receipt → BLOCKED" \
|
|
66
|
+
"$(envelope "$BROWSER" "$DOCS/quote-job-2.pdf")" 2
|
|
67
|
+
|
|
68
|
+
run_case "B: client quote PDF, receipt present → ALLOWED" \
|
|
69
|
+
"$(envelope "$BROWSER" "$DOCS/quote-job-1.pdf")" 0
|
|
70
|
+
|
|
71
|
+
run_case "C: client breakdown PDF, no receipt → BLOCKED" \
|
|
72
|
+
"$(envelope "$BROWSER" "$DOCS/quote-job-3-breakdown.pdf")" 2
|
|
73
|
+
|
|
74
|
+
run_case "D: client breakdown PDF, receipt present → ALLOWED" \
|
|
75
|
+
"$(envelope "$BROWSER" "$DOCS/quote-job-1-breakdown.pdf")" 0
|
|
76
|
+
|
|
77
|
+
run_case "E: non-quote PDF in documents scope → ALLOWED" \
|
|
78
|
+
"$(envelope "$BROWSER" "$DOCS/brochure.pdf")" 0
|
|
79
|
+
|
|
80
|
+
run_case "F: internal quote PDF (admin scope) → ALLOWED" \
|
|
81
|
+
"$(envelope "$BROWSER" "$ADMIN/quote-job-2-internal.pdf")" 0
|
|
82
|
+
|
|
83
|
+
run_case "G: quote-named PDF outside client scope → ALLOWED" \
|
|
84
|
+
"$(envelope "$BROWSER" "$ACCT/quote-job-2.pdf")" 0
|
|
85
|
+
|
|
86
|
+
run_case "H: empty stdin → ALLOWED (fail-open)" \
|
|
87
|
+
"" 0
|
|
88
|
+
|
|
89
|
+
run_case "I: non-browser tool → ALLOWED" \
|
|
90
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"/x/memory/users/44700900000/documents/quote-job-2.pdf"}}' 0
|
|
91
|
+
|
|
92
|
+
run_case "J: jobId path traversal → BLOCKED" \
|
|
93
|
+
"$(envelope "$BROWSER" "$DOCS/quote-...pdf")" 2
|
|
94
|
+
|
|
95
|
+
# K: a block emits the op=bypass fault line on stderr.
|
|
96
|
+
STDERR=$( cd "$ACCT" && printf '%s' "$(envelope "$BROWSER" "$DOCS/quote-job-2.pdf")" | bash "$HOOK" 2>&1 >/dev/null )
|
|
97
|
+
if printf '%s' "$STDERR" | grep -q '\[quote-render\] op=bypass jobId=job-2'; then
|
|
98
|
+
echo "PASS: K: block emits op=bypass jobId=job-2"
|
|
99
|
+
PASS=$((PASS + 1))
|
|
100
|
+
else
|
|
101
|
+
echo "FAIL: K: op=bypass line absent (got: $STDERR)" >&2
|
|
102
|
+
FAIL=$((FAIL + 1))
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
echo "──────── quote-render-gate test summary ────────"
|
|
106
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
107
|
+
[[ "$FAIL" -eq 0 ]]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# quote-render-gate — PreToolUse gate on browser-pdf-save.
|
|
3
|
+
#
|
|
4
|
+
# A customer only ever receives the two client-facing quote documents from the
|
|
5
|
+
# customer documents scope (memory/users/<phone>/documents/). This gate refuses
|
|
6
|
+
# to print one to PDF unless the deterministic quote-render tool actually
|
|
7
|
+
# produced it — proven by the render receipt quote-render writes on success
|
|
8
|
+
# (quoting/jobs/<jobId>/render-receipt.json). No receipt for the job means the
|
|
9
|
+
# quote was hand-authored off the deterministic path, so the fixed-layout
|
|
10
|
+
# guarantee and the reconcile/margin/link gates never ran; the PDF is refused and
|
|
11
|
+
# the bypass is surfaced as [quote-render] op=bypass, not delivered.
|
|
12
|
+
#
|
|
13
|
+
# Scope: only a .pdf under memory/users/<phone>/documents/ named
|
|
14
|
+
# quote-<jobId>.pdf or quote-<jobId>-breakdown.pdf is gated. Every other PDF —
|
|
15
|
+
# brochures, invoices, the admin-scope internal view — passes untouched.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail OPEN on an
|
|
18
|
+
# uninspectable call (browser-pdf-save is a general-purpose tool; the bypass we
|
|
19
|
+
# guard is an agent skipping quote-render, and the agent never controls this
|
|
20
|
+
# hook's stdin, so failing open here opens no bypass). No task numbers or
|
|
21
|
+
# internal refs in any operator-visible string.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
# No envelope (tty or empty stdin) → allow: cannot inspect, must not brick PDFs.
|
|
26
|
+
if [ -t 0 ]; then exit 0; fi
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
[ -z "$INPUT" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Print the jobId of a gated client-quote artefact, or nothing for any other
|
|
31
|
+
# call. The customer documents scope is a path convention, so match on it
|
|
32
|
+
# structurally rather than resolving symlinks.
|
|
33
|
+
JOBID=$(INPUT="$INPUT" python3 - <<'PY' 2>/dev/null || true
|
|
34
|
+
import os, json, re, sys
|
|
35
|
+
try:
|
|
36
|
+
d = json.loads(os.environ["INPUT"])
|
|
37
|
+
except Exception:
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
tool = d.get("tool_name", "") or ""
|
|
40
|
+
if not tool.endswith("browser-pdf-save"):
|
|
41
|
+
sys.exit(0)
|
|
42
|
+
path = (d.get("tool_input", {}) or {}).get("path", "") or ""
|
|
43
|
+
if not path:
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
norm = os.path.normpath(path)
|
|
46
|
+
m = re.search(r'(?:^|/)memory/users/[^/]+/documents/quote-(.+?)(?:-breakdown)?\.pdf$', norm)
|
|
47
|
+
if not m:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
print(m.group(1))
|
|
50
|
+
PY
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Not a gated client-quote artefact → allow.
|
|
54
|
+
[ -z "$JOBID" ] && exit 0
|
|
55
|
+
|
|
56
|
+
# A jobId is a single path segment. A separator or traversal in it never keys a
|
|
57
|
+
# legitimate receipt, so treat it as a bypass rather than resolving a stray file.
|
|
58
|
+
case "$JOBID" in
|
|
59
|
+
*/*|*..*)
|
|
60
|
+
echo "[quote-render] op=bypass jobId=$JOBID reason=bad-jobid" >&2
|
|
61
|
+
echo "Blocked: this looks like a client quote PDF but its name does not resolve to a job. Produce the quote with the quote-render tool, which files the document and its render receipt, then print the path it returns." >&2
|
|
62
|
+
exit 2
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# The receipt lives beside the job's figures, written by quote-render only after
|
|
67
|
+
# every gate passed. Its presence is the proof the deterministic path ran.
|
|
68
|
+
if [ -f "$PWD/quoting/jobs/$JOBID/render-receipt.json" ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
echo "[quote-render] op=bypass jobId=$JOBID detail=client quote PDF has no render receipt" >&2
|
|
73
|
+
echo "Blocked: this client quote was not produced by the quote-render tool, so its fixed layout and the reconcile, margin-leak and link-liveness gates never ran. Run quote-render for this job (it files the document and its receipt), then print the client HTML path it returns to PDF." >&2
|
|
74
|
+
exit 2
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:8718b7135f79829576ed6a21c6a0bb39a62eaf8a8e772cec37124c3aa44dfdc7
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2044,7 +2044,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
2044
2044
|
|
|
2045
2045
|
1. **No app to register.** The client ID ships with the brand (`brand.json#outlookClientId`), and the installer stamps it onto the session-manager env, so there is zero operator Azure work. On a brand that ships an empty value the plugin stays inert until its vendor app exists — see `platform/plugins/outlook/references/auth.md` for the one-time-per-brand vendor provisioning runbook (a vendor step, not an operator or end-user step), including setting **Allow public client flows** to Yes.
|
|
2046
2046
|
2. **Per account: register the Outlook account** — in admin chat, ask the agent to "register my Outlook account". The agent runs `outlook-account-register`, which returns a short user code and the URL `microsoft.com/devicelogin` and relays both to you.
|
|
2047
|
-
3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds.
|
|
2047
|
+
3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds. Nothing else to do: the server finishes the sign-in automatically once you consent, usually within about half a minute, and the mailbox is then connected.
|
|
2048
2048
|
4. **Done.** Subsequent tool calls (mail, calendar, contacts) use the persisted refresh token transparently.
|
|
2049
2049
|
5. **Several mailboxes on one account.** Register again to add a second mailbox; it is stored alongside the first, not in place of it. Every operational tool then takes an optional `mailbox` argument — an email address or graphUserId. With one mailbox, omit it. With two or more, name it; omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows every connected mailbox. Only one registration runs at a time per account; starting another while one is live returns an in-progress signal.
|
|
2050
2050
|
|
|
@@ -2054,8 +2054,8 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
2054
2054
|
|------|---------|
|
|
2055
2055
|
| `outlook-account-register` | Start the device-code flow for this account. Returns a user code + `microsoft.com/devicelogin` URL immediately; does not block. |
|
|
2056
2056
|
| `outlook-account-register-poll` | Finish the device-code flow. One check per call: returns pending, registered, expired, or denied. |
|
|
2057
|
-
| `outlook-mail-list` |
|
|
2058
|
-
| `outlook-mail-search` | Microsoft Graph `$search` over the mailbox. Preview-only, like `outlook-mail-list`. |
|
|
2057
|
+
| `outlook-mail-list` | Mail newest-first. Default top=25, folder=Inbox. `folder` takes a well-known name or alias (case/space-insensitive): Inbox, Sent Items/sent, Drafts, Deleted Items/trash, Junk Email/junk/spam, Archive, Outbox — or a raw folder id. Custom folder names are not resolved. `since`/`before` (ISO date-times) narrow by receipt date. The 250-per-page figure is not a ceiling: the reply carries `nextCursor` when older mail remains — pass it back as `cursor` to page further back. Filter by sender/subject via `outlook-mail-search`. |
|
|
2058
|
+
| `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
|
|
2059
2059
|
| `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
|
|
2060
2060
|
| `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
|
|
2061
2061
|
| `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
|
|
@@ -48,7 +48,7 @@ echo "\$@" >> "\$SB/curl.log"
|
|
|
48
48
|
url=""; for a in "\$@"; do case "\$a" in https://*) url="\$a";; esac; done
|
|
49
49
|
case "\$url" in
|
|
50
50
|
*"/tokens/permission_groups"*)
|
|
51
|
-
echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"},{"id":"pg_dns","name":"DNS Write"},{"id":"pg_acc1","name":"Access: Apps and Policies Write"},{"id":"pg_acc2","name":"Access: Apps and Policies Write"}]}'
|
|
51
|
+
echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"},{"id":"pg_r2","name":"Workers R2 Storage Write"},{"id":"pg_dns","name":"DNS Write"},{"id":"pg_acc1","name":"Access: Apps and Policies Write"},{"id":"pg_acc2","name":"Access: Apps and Policies Write"}]}'
|
|
52
52
|
;;
|
|
53
53
|
*"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
|
|
54
54
|
*"/tokens"*) echo '{"success":true,"result":{"id":"tok1","value":"'"$SECRET_VALUE"'"}}';;
|
|
@@ -353,5 +353,29 @@ grep -q "^CF_PAGES_D1_TOKEN=" "$OVERRIDE" && ok "override env received the minte
|
|
|
353
353
|
echo "$ERR" | grep -q "src=house" && ok "override reports src=house" || bad "override src: $ERR"
|
|
354
354
|
teardown
|
|
355
355
|
|
|
356
|
+
# 17. storage scope (Task 1670): the broker's data token needs BOTH D1 Write and
|
|
357
|
+
# Workers R2 Storage Write, persisted under CF_STORAGE_TOKEN.
|
|
358
|
+
setup "cfat_master_abc"
|
|
359
|
+
run "storage mint D1+R2" storage 0
|
|
360
|
+
[ "$OUT" = "CF_STORAGE_TOKEN" ] && ok "storage stdout is CF_STORAGE_TOKEN" || bad "storage out='$OUT'"
|
|
361
|
+
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
|
|
362
|
+
echo "$POSTLINE" | grep -q "pg_d1" && ok "storage minted D1 id" || bad "storage no D1 id: $POSTLINE"
|
|
363
|
+
echo "$POSTLINE" | grep -q "pg_r2" && ok "storage minted R2 id" || bad "storage no R2 id: $POSTLINE"
|
|
364
|
+
grep -q "^CF_STORAGE_TOKEN=" "$SECRETS" && ok "storage token persisted" || bad "storage not persisted"
|
|
365
|
+
no_leak "storage mint"
|
|
366
|
+
teardown
|
|
367
|
+
|
|
368
|
+
# 18. calendar-d1 scope (Task 1670): the calendar's data token needs D1 Write
|
|
369
|
+
# ONLY (no R2), persisted under CF_CALENDAR_D1_TOKEN.
|
|
370
|
+
setup "cfat_master_abc"
|
|
371
|
+
run "calendar-d1 mint D1 only" calendar-d1 0
|
|
372
|
+
[ "$OUT" = "CF_CALENDAR_D1_TOKEN" ] && ok "calendar-d1 stdout is CF_CALENDAR_D1_TOKEN" || bad "calendar-d1 out='$OUT'"
|
|
373
|
+
POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
|
|
374
|
+
echo "$POSTLINE" | grep -q "pg_d1" && ok "calendar-d1 minted D1 id" || bad "calendar-d1 no D1 id: $POSTLINE"
|
|
375
|
+
echo "$POSTLINE" | grep -q "pg_r2" && bad "calendar-d1 wrongly minted R2 id: $POSTLINE" || ok "calendar-d1 no R2 id"
|
|
376
|
+
grep -q "^CF_CALENDAR_D1_TOKEN=" "$SECRETS" && ok "calendar-d1 token persisted" || bad "calendar-d1 not persisted"
|
|
377
|
+
no_leak "calendar-d1 mint"
|
|
378
|
+
teardown
|
|
379
|
+
|
|
356
380
|
echo "----"; echo "PASS=$PASS FAIL=$FAIL"
|
|
357
381
|
[ "$FAIL" -eq 0 ]
|