@rubytech/create-sitedesk-code 0.1.511 → 0.1.513
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-07-27-task-2028-declared-file-write-deny.md +303 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +126 -0
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts +5 -0
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-schema-regions/dist/index.js +5 -0
- package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -1
- package/payload/platform/lib/account-schema-regions/src/index.ts +5 -0
- package/payload/platform/package.json +3 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +38 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +11 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +25 -1
- package/payload/platform/plugins/admin/skills/deck-pages/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +1 -1
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +15 -34
- package/payload/platform/plugins/business-assistant/skills/e-sign/stamp.mjs +269 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +1 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +5 -5
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +2 -2
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +26 -0
- package/payload/platform/scripts/check-no-esm-require.mjs +6 -0
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +40 -4
- package/payload/platform/scripts/logs-read.sh +154 -50
- package/payload/platform/scripts/logs-read.test.sh +276 -0
- package/payload/platform/scripts/provision-worktree.sh +80 -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 +13 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -3
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +17 -11
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
- package/payload/server/{chunk-2WAXM5N2.js → chunk-S6HYTIU3.js} +2 -3
- package/payload/server/maxy-edge.js +7 -3
- package/payload/server/public/assets/admin-6QN-k3zQ.js +1 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.br +0 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.gz +0 -0
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +860 -384
- package/payload/server/public/assets/admin-Do53K90C.js +0 -1
- package/payload/server/public/assets/admin-Do53K90C.js.br +0 -0
- package/payload/server/public/assets/admin-Do53K90C.js.gz +0 -0
|
@@ -394,9 +394,9 @@ export default function DeckLayout({ children }: { children: React.ReactNode })
|
|
|
394
394
|
|
|
395
395
|
The page component (`app/deck/page.tsx`) is a `"use client"` component containing the slides, the download function, and the download button. Keep the layout as a server component for metadata; the "use client" directive belongs on the page, not the layout.
|
|
396
396
|
|
|
397
|
-
## Reference
|
|
397
|
+
## Reference arc
|
|
398
398
|
|
|
399
|
-
A complete
|
|
399
|
+
A complete deck runs eight slides — title, problem, market, solution, differentiation, roadmap, competitive landscape, and team/ask. That is the standard pitch-deck arc; deviate only when the subject demands it.
|
|
400
400
|
|
|
401
401
|
## Checklist before shipping
|
|
402
402
|
|
|
@@ -83,7 +83,7 @@ Show the user the complete SKILL.md content and each reference file. Ask them to
|
|
|
83
83
|
|
|
84
84
|
Use `store-skill` to write the composed skill to disk as part of an admin-managed plugin. The tool computes the path internally — supply the names and content only.
|
|
85
85
|
|
|
86
|
-
Pick a `pluginName` that groups related operator-authored skills (e.g. `
|
|
86
|
+
Pick a `pluginName` that groups related operator-authored skills (e.g. `sales-skills`, `my-utils`). Reuse the same `pluginName` across calls so the skill joins existing operator skills under one plugin. If this is the first skill in a new plugin, the tool creates the `PLUGIN.md` automatically.
|
|
87
87
|
|
|
88
88
|
```
|
|
89
89
|
store-skill({
|
|
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-27 (0.1.513)
|
|
13
|
+
|
|
14
|
+
- Fixed sign-in for sub-accounts: the browser session bridge now checks that you belong to the account you asked for, instead of only ever accepting the first one it found.
|
|
15
|
+
- WhatsApp account checks now use the account's real identifier rather than a folder name, so conversations are attributed to the right account.
|
|
16
|
+
- The saved session-title cache is now pruned as the sessions list is walked, so it no longer grows without limit.
|
|
17
|
+
|
|
18
|
+
## 2026-07-27 (0.1.512)
|
|
19
|
+
|
|
20
|
+
- Signed PDFs keep their clickable links and selectable text. The e-sign stamp no longer falls back to flattening each page into an image, which had been discarding every link in the document.
|
|
21
|
+
- The sessions list is much faster: it no longer reads every account's transcripts in full just to build the list, and titles now come from a cache.
|
|
22
|
+
- Fixed a dashboard error that repeated once a minute when you were switched to a sub-account, caused by asking for a conversations list only the main account is allowed to read.
|
|
23
|
+
|
|
12
24
|
## 2026-07-26 (0.1.511)
|
|
13
25
|
|
|
14
26
|
- Fixed a dashboard bug where the Host CPU card could sit on a loading skeleton for a full minute after its figure had already arrived, even though the sidebar was showing the same number.
|
|
@@ -477,47 +477,28 @@ Dispatch runs **only** inside a live operator session on the account's own devic
|
|
|
477
477
|
|
|
478
478
|
2. **For each unswept row, in order — stamp, send, then mark that one row swept:**
|
|
479
479
|
|
|
480
|
-
- **Stamp a per-signer copy from the persisted base
|
|
481
|
-
|
|
482
|
-
**Pre-flight: the page-copying library must resolve before any stamp runs.** A stamp attempted without it is the exact history this guards against, where a prior session met the missing library by rasterising instead of installing one:
|
|
480
|
+
- **Stamp a per-signer copy from the persisted base with the platform's stamp script.** Dispatch must **never fetch the deployed page**: it may sit behind **Cloudflare Access**, so a headless fetch hits the Access login screen, not the document, and the PDF is garbage. The render-once base (§ 5) is what removes that dependency. The stamp is platform code, not something to compose at dispatch time:
|
|
483
481
|
|
|
484
482
|
```bash
|
|
485
|
-
|
|
483
|
+
DIR="${ACCOUNT_DIR}/e-sign/<DOC_REF>" \
|
|
484
|
+
CERT='{"name":"…","title":"…","token":"…","doc_ref":"…","accepted_at":"…","method":"…","assertion":"…"}' \
|
|
485
|
+
node "$PLATFORM_ROOT/plugins/business-assistant/skills/e-sign/stamp.mjs"
|
|
486
486
|
```
|
|
487
487
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const cert = JSON.parse(process.env.CERT); // {name,title,token,doc_ref,accepted_at,method,assertion}
|
|
498
|
-
const pdf = await PDFDocument.load(readFileSync(`${dir}/base.pdf`));
|
|
499
|
-
const font = await pdf.embedFont(StandardFonts.Helvetica);
|
|
500
|
-
const page = pdf.addPage();
|
|
501
|
-
const lines = [
|
|
502
|
-
"Electronic Acceptance Certificate",
|
|
503
|
-
`Name: ${cert.name}`,
|
|
504
|
-
`Title: ${cert.title ?? "-"}`,
|
|
505
|
-
`Doc ref: ${cert.doc_ref}`,
|
|
506
|
-
`Token: ${cert.token}`,
|
|
507
|
-
`Accepted: ${cert.accepted_at}`,
|
|
508
|
-
`Method: ${cert.method}`, // e.g. "simple electronic signature (UK ECA 2000 s.7 / UK eIDAS)"
|
|
509
|
-
"",
|
|
510
|
-
"Assertion the signer ticked:",
|
|
511
|
-
cert.assertion, // the verbatim intent/consent text from § 2
|
|
512
|
-
];
|
|
513
|
-
let y = page.getHeight() - 60;
|
|
514
|
-
for (const ln of lines) { page.drawText(ln, { x: 50, y, size: 11, font }); y -= 20; }
|
|
515
|
-
writeFileSync(`${dir}/signed-${cert.token}.pdf`, await pdf.save());
|
|
488
|
+
It loads `base.pdf`, appends the **Electronic Acceptance Certificate** page, and writes `signed-<token>.pdf` beside the base. No browser, no network, and no library to install: `pdf-lib` is a platform dependency that every install provides.
|
|
489
|
+
|
|
490
|
+
**Never write your own stamp.** This is not a style preference. A session that found no PDF library on the box once answered it by rasterising the base to a 200 DPI image and redrawing it, which discarded every link annotation and all selectable text. Four signers were sent flattened copies with dead links and nothing logged it. Rasterising a base, whether through ghostscript or any other image-then-redraw route, is prohibited whatever is or is not installed on the device.
|
|
491
|
+
|
|
492
|
+
**Pre-flight.** An absent script means the install is out of date, and the single remediation is to upgrade it through the installer. Never hand-install a library on a device, and never substitute a stamp of your own while waiting:
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
test -f "$PLATFORM_ROOT/plugins/business-assistant/skills/e-sign/stamp.mjs" \
|
|
496
|
+
|| echo "e-sign stamp missing: upgrade this install, do not improvise a stamp"
|
|
516
497
|
```
|
|
517
498
|
|
|
518
|
-
The certificate fields come **from the D1 row**, not a re-fetch: `name`, `title` (an operator-defined field if present), `token`, `doc_ref`, `accepted_at`, the signing `method`, and the **verbatim intent/consent assertion** the signer ticked (§ 2). That makes the artifact self-contained evidence of who accepted, when, and on what stated basis.
|
|
499
|
+
The certificate fields come **from the D1 row**, not a re-fetch: `name`, `title` (an operator-defined field if present), `token`, `doc_ref`, `accepted_at`, the signing `method`, and the **verbatim intent/consent assertion** the signer ticked (§ 2). That makes the artifact self-contained evidence of who accepted, when, and on what stated basis. The script refuses to run on an incomplete certificate.
|
|
519
500
|
|
|
520
|
-
**
|
|
501
|
+
**The fidelity gate lives inside the script, so it cannot be skipped.** Before writing anything it checks the stamped copy against the base on three counts: every base page must come through byte-identical, the page count must be the base plus the certificate pages, and the `/URI` link count must not drop. On any regression it writes no file and exits non-zero, in which case both sends are skipped and the row stays `swept = 0` so the next sweep re-surfaces it. That converts a link-stripped copy from a no-event failure, where nothing logs until a signer clicks a dead link, into a refusal at stamp time. A zero exit prints one `[e-sign-stamp]` line carrying the base and stamped `/URI` counts; keep it with the dispatch record so a fidelity regression is a number rather than a complaint.
|
|
521
502
|
|
|
522
503
|
- **Send twice via `email-send`**, each carrying `doc_ref`, `name`, `token`, `accepted_at`, and the **stamped** PDF (`signed-<token>.pdf`) as an attachment:
|
|
523
504
|
- once to the **business owner's configured address** (read from configured business identity — never hard-code a recipient in this workflow);
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stamp.mjs — the e-sign per-signer stamp.
|
|
4
|
+
*
|
|
5
|
+
* Loads the persisted `base.pdf` for one DOC_REF, appends an Electronic
|
|
6
|
+
* Acceptance Certificate page carrying the D1 row's fields, and writes
|
|
7
|
+
* `signed-<token>.pdf` beside the base. No browser, no network.
|
|
8
|
+
*
|
|
9
|
+
* This ships with the platform rather than being written out by the agent at
|
|
10
|
+
* dispatch time, because the agent-authored version was the defect: a session
|
|
11
|
+
* that met a missing PDF library answered it by rasterising the base to PNG and
|
|
12
|
+
* redrawing it, which discarded every link annotation and all selectable text.
|
|
13
|
+
* Signers received flattened copies with dead links and nothing logged it
|
|
14
|
+
* (Task 1921). `pdf-lib` is a platform dependency, installed on every device by
|
|
15
|
+
* the installer's `npm install` in `platform/`, so the missing-library branch
|
|
16
|
+
* that led to rasterising cannot arise.
|
|
17
|
+
*
|
|
18
|
+
* The fidelity gate below is what makes that guarantee enforceable rather than
|
|
19
|
+
* merely intended: the stamped copy is written only when every base page came
|
|
20
|
+
* through byte-identical and no `/URI` link was lost. A rebuild-from-image
|
|
21
|
+
* implementation cannot pass it.
|
|
22
|
+
*
|
|
23
|
+
* Invocation (the skill's § 6 dispatch step):
|
|
24
|
+
*
|
|
25
|
+
* DIR=<accountDir>/e-sign/<DOC_REF> \
|
|
26
|
+
* CERT='{"name":…,"title":…,"token":…,"doc_ref":…,"accepted_at":…,"method":…,"assertion":…}' \
|
|
27
|
+
* node "$PLATFORM_ROOT/plugins/business-assistant/skills/e-sign/stamp.mjs"
|
|
28
|
+
*
|
|
29
|
+
* Exit 0 writes the copy and prints one `[e-sign-stamp]` record line carrying
|
|
30
|
+
* the base and stamped `/URI` counts. Any non-zero exit means no copy was
|
|
31
|
+
* written and the acceptance row must stay `swept = 0`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
import { pathToFileURL } from "node:url";
|
|
38
|
+
import { PDFArray, PDFDocument, PDFName, PDFRef, StandardFonts } from "pdf-lib";
|
|
39
|
+
|
|
40
|
+
const MARGIN = 50;
|
|
41
|
+
const TITLE_SIZE = 14;
|
|
42
|
+
const BODY_SIZE = 11;
|
|
43
|
+
const LINE_HEIGHT = 20;
|
|
44
|
+
|
|
45
|
+
/** The certificate's fields, in the order § 6 of the skill declares them. */
|
|
46
|
+
export function certificateLines(cert) {
|
|
47
|
+
return [
|
|
48
|
+
{ text: "Electronic Acceptance Certificate", size: TITLE_SIZE },
|
|
49
|
+
{ text: `Name: ${cert.name}` },
|
|
50
|
+
{ text: `Title: ${cert.title ?? "-"}` },
|
|
51
|
+
{ text: `Doc ref: ${cert.doc_ref}` },
|
|
52
|
+
{ text: `Token: ${cert.token}` },
|
|
53
|
+
{ text: `Accepted: ${cert.accepted_at}` },
|
|
54
|
+
{ text: `Method: ${cert.method}` },
|
|
55
|
+
{ text: "" },
|
|
56
|
+
{ text: "Assertion the signer ticked:" },
|
|
57
|
+
{ text: cert.assertion, wrap: true },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Break `text` so each line fits `maxWidth` at `size`. Long unbreakable tokens
|
|
62
|
+
* are emitted on their own line rather than dropped. */
|
|
63
|
+
export function wrapText(text, font, size, maxWidth) {
|
|
64
|
+
const words = String(text).split(/\s+/).filter(Boolean);
|
|
65
|
+
if (words.length === 0) return [""];
|
|
66
|
+
const lines = [];
|
|
67
|
+
let line = "";
|
|
68
|
+
for (const word of words) {
|
|
69
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
70
|
+
if (font.widthOfTextAtSize(candidate, size) <= maxWidth || !line) {
|
|
71
|
+
line = candidate;
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(line);
|
|
74
|
+
line = word;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (line) lines.push(line);
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolve(doc, obj) {
|
|
82
|
+
return obj instanceof PDFRef ? doc.context.lookup(obj) : obj;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** SHA-256 of a page's concatenated content streams. Two pages with the same
|
|
86
|
+
* digest carry the same drawing operators; a page re-rendered from an image
|
|
87
|
+
* never matches the page it replaced. */
|
|
88
|
+
export function pageContentDigest(doc, page) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
const collect = (obj) => {
|
|
91
|
+
const value = resolve(doc, obj);
|
|
92
|
+
if (!value) return;
|
|
93
|
+
if (value instanceof PDFArray) {
|
|
94
|
+
for (const entry of value.asArray()) collect(entry);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (typeof value.getContents === "function") {
|
|
98
|
+
parts.push(Buffer.from(value.getContents()));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (value.contents) parts.push(Buffer.from(value.contents));
|
|
102
|
+
};
|
|
103
|
+
collect(page.node.get(PDFName.of("Contents")));
|
|
104
|
+
return createHash("sha256").update(Buffer.concat(parts)).digest("hex");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Annotations on `page` whose action dictionary carries a `/URI` — the
|
|
108
|
+
* external hyperlinks the signer is meant to be able to click. */
|
|
109
|
+
export function uriLinkCount(doc, page) {
|
|
110
|
+
const annots = resolve(doc, page.node.get(PDFName.of("Annots")));
|
|
111
|
+
if (!(annots instanceof PDFArray)) return 0;
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (const entry of annots.asArray()) {
|
|
114
|
+
const annot = resolve(doc, entry);
|
|
115
|
+
if (typeof annot?.get !== "function") continue;
|
|
116
|
+
const action = resolve(doc, annot.get(PDFName.of("A")));
|
|
117
|
+
if (typeof action?.get !== "function") continue;
|
|
118
|
+
if (action.get(PDFName.of("URI")) !== undefined) count += 1;
|
|
119
|
+
}
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Per-page digests and `/URI` counts, in page order. */
|
|
124
|
+
export async function pageFingerprints(bytes) {
|
|
125
|
+
const doc = await PDFDocument.load(bytes);
|
|
126
|
+
return doc.getPages().map((page) => ({
|
|
127
|
+
digest: pageContentDigest(doc, page),
|
|
128
|
+
uris: uriLinkCount(doc, page),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reject the stamped bytes unless every base page survived unaltered.
|
|
134
|
+
*
|
|
135
|
+
* Three conditions, each naming a distinct way a stamp can destroy the document
|
|
136
|
+
* it was supposed to preserve:
|
|
137
|
+
* 1. page count, which catches base pages lost between building the document
|
|
138
|
+
* and reading back what was serialised;
|
|
139
|
+
* 2. content digest, which catches a page redrawn (the rasterisation failure);
|
|
140
|
+
* 3. per-page `/URI` count, which catches annotations discarded while the
|
|
141
|
+
* visible page survives. Compared page by page rather than as a document
|
|
142
|
+
* total, so links moved off one page cannot be masked by another.
|
|
143
|
+
*/
|
|
144
|
+
export async function assertFidelity(baseBytes, stampedBytes, addedPages) {
|
|
145
|
+
const base = await pageFingerprints(baseBytes);
|
|
146
|
+
const stamped = await pageFingerprints(stampedBytes);
|
|
147
|
+
|
|
148
|
+
const expected = base.length + addedPages;
|
|
149
|
+
if (stamped.length !== expected) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`page count regressed: base ${base.length} + ${addedPages} certificate page(s) ` +
|
|
152
|
+
`should give ${expected}, stamped copy has ${stamped.length}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < base.length; i += 1) {
|
|
157
|
+
if (stamped[i].digest !== base[i].digest) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`page ${i + 1} was re-rendered rather than copied — its content stream does not ` +
|
|
160
|
+
`match the base. A stamp must copy existing pages; rasterising or redrawing them ` +
|
|
161
|
+
`discards links and selectable text.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (stamped[i].uris < base[i].uris) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`link annotations lost on page ${i + 1}: base carries ${base[i].uris} /URI link(s), ` +
|
|
167
|
+
`stamped copy carries ${stamped[i].uris}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const baseUris = base.reduce((n, p) => n + p.uris, 0);
|
|
173
|
+
const stampedUris = stamped.reduce((n, p) => n + p.uris, 0);
|
|
174
|
+
return { basePages: base.length, stampedPages: stamped.length, baseUris, stampedUris };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Load the base, append the certificate, and return the stamped bytes plus the
|
|
178
|
+
* number of pages the certificate needed. */
|
|
179
|
+
export async function buildStamped(baseBytes, cert) {
|
|
180
|
+
const pdf = await PDFDocument.load(baseBytes);
|
|
181
|
+
const basePages = pdf.getPageCount();
|
|
182
|
+
if (basePages === 0) throw new Error("base.pdf has no pages");
|
|
183
|
+
|
|
184
|
+
const font = await pdf.embedFont(StandardFonts.Helvetica);
|
|
185
|
+
const [width, height] = [pdf.getPage(0).getWidth(), pdf.getPage(0).getHeight()];
|
|
186
|
+
const maxWidth = width - MARGIN * 2;
|
|
187
|
+
|
|
188
|
+
let page = pdf.addPage([width, height]);
|
|
189
|
+
let y = height - MARGIN - LINE_HEIGHT;
|
|
190
|
+
const draw = (text, size) => {
|
|
191
|
+
if (y < MARGIN) {
|
|
192
|
+
page = pdf.addPage([width, height]);
|
|
193
|
+
y = height - MARGIN - LINE_HEIGHT;
|
|
194
|
+
}
|
|
195
|
+
if (text) page.drawText(text, { x: MARGIN, y, size, font });
|
|
196
|
+
y -= LINE_HEIGHT;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
for (const line of certificateLines(cert)) {
|
|
201
|
+
const size = line.size ?? BODY_SIZE;
|
|
202
|
+
if (line.wrap) {
|
|
203
|
+
for (const wrapped of wrapText(line.text, font, size, maxWidth)) draw(wrapped, size);
|
|
204
|
+
} else {
|
|
205
|
+
draw(line.text, size);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Helvetica is WinAnsi-encoded, so a signer name or assertion carrying a
|
|
210
|
+
// character outside CP-1252 cannot be drawn. Say so in terms that keep the
|
|
211
|
+
// caller on the supported path: the remedy is a platform change that embeds
|
|
212
|
+
// a Unicode font, never a stamp improvised with a different library.
|
|
213
|
+
throw new Error(
|
|
214
|
+
`the certificate carries a character the standard font cannot encode (${err.message}). ` +
|
|
215
|
+
`This needs a platform fix that embeds a Unicode font. Do not improvise a stamp: ` +
|
|
216
|
+
`leave the row swept = 0 and report it.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { bytes: await pdf.save(), addedPages: pdf.getPageCount() - basePages };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const REQUIRED_CERT_FIELDS = ["name", "token", "doc_ref", "accepted_at", "method", "assertion"];
|
|
224
|
+
|
|
225
|
+
async function main() {
|
|
226
|
+
const dir = process.env.DIR;
|
|
227
|
+
if (!dir) throw new Error("DIR is not set — it must be <accountDir>/e-sign/<DOC_REF>");
|
|
228
|
+
if (!process.env.CERT) throw new Error("CERT is not set — it must be the D1 row's certificate fields as JSON");
|
|
229
|
+
|
|
230
|
+
let cert;
|
|
231
|
+
try {
|
|
232
|
+
cert = JSON.parse(process.env.CERT);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
throw new Error(`CERT is not valid JSON: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
const missing = REQUIRED_CERT_FIELDS.filter((field) => cert[field] === undefined || cert[field] === "");
|
|
237
|
+
if (missing.length > 0) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`CERT is missing ${missing.join(", ")} — every field comes from the acceptance row, never a re-fetch`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const basePath = join(dir, "base.pdf");
|
|
244
|
+
const baseBytes = readFileSync(basePath);
|
|
245
|
+
const { bytes, addedPages } = await buildStamped(baseBytes, cert);
|
|
246
|
+
const counts = await assertFidelity(baseBytes, bytes, addedPages);
|
|
247
|
+
|
|
248
|
+
const outPath = join(dir, `signed-${cert.token}.pdf`);
|
|
249
|
+
writeFileSync(outPath, bytes);
|
|
250
|
+
console.log(
|
|
251
|
+
`[e-sign-stamp] op=stamped doc_ref=${cert.doc_ref} token=${cert.token} ` +
|
|
252
|
+
`base_pages=${counts.basePages} stamped_pages=${counts.stampedPages} ` +
|
|
253
|
+
`base_uris=${counts.baseUris} stamped_uris=${counts.stampedUris} out=${outPath}`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Node realpaths the ESM entry point, so `import.meta.url` is symlink-resolved
|
|
258
|
+
// and `process.argv[1]` is not. Comparing them raw makes a symlinked
|
|
259
|
+
// PLATFORM_ROOT skip main() and exit 0 having stamped nothing, which is exactly
|
|
260
|
+
// the silent success this script exists to make impossible.
|
|
261
|
+
const invokedDirectly =
|
|
262
|
+
process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
263
|
+
|
|
264
|
+
if (invokedDirectly) {
|
|
265
|
+
main().catch((err) => {
|
|
266
|
+
console.error(`[e-sign-stamp] op=failed reason="${err.message}" — no copy written, leave the row swept = 0`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloudflare
|
|
3
3
|
description: Cloudflare operations — tunnel setup/reset, DNS, Pages hosting, D1 data capture, and dashboard guidance. Zero agent-facing MCP tools; every operation is the agent invoking `cloudflared` or `wrangler` directly via Bash, calling the Cloudflare API with a reused per-scope narrow token, or quoting a dashboard click-path the operator performs themselves.
|
|
4
|
-
account-owned-files: [{"file": "data-portal.json", "description": "The account's
|
|
4
|
+
account-owned-files: [{"file": "data-portal.json", "writtenBy": "agent", "description": "The account's portal config and folder index. Authored by the agent under the data-portal skill, which is also the only path that changes it; bin/portal-index-push.mjs reads it on every push."}]
|
|
5
5
|
tools: []
|
|
6
6
|
mcp-manifest: skip
|
|
7
7
|
---
|
|
@@ -78,7 +78,7 @@ export function resolveExposedDirs(schemaText, exposeFolders = []) {
|
|
|
78
78
|
collisions: /** @type {string[]} */ ([]),
|
|
79
79
|
exposed: /** @type {string[]} */ ([]),
|
|
80
80
|
}
|
|
81
|
-
// Fail CLOSED. fs-schema-guard.sh:
|
|
81
|
+
// Fail CLOSED. fs-schema-guard.sh:77-80 fails open on a missing schema so an
|
|
82
82
|
// unseeded legacy account is not write-blocked; a read surface facing a
|
|
83
83
|
// client must do the opposite. Do not "fix" this to match the guard.
|
|
84
84
|
if (typeof schemaText !== 'string' || schemaText.length === 0) return empty
|
package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md
CHANGED
|
@@ -40,18 +40,18 @@ This reference invokes `mcp__plugin_memory_memory__memory-update` once with the
|
|
|
40
40
|
{
|
|
41
41
|
"nodeId": "<elementId of :UserProfile from owner-confirmation>",
|
|
42
42
|
"properties": {
|
|
43
|
-
"givenName": "
|
|
44
|
-
"familyName": "
|
|
43
|
+
"givenName": "<given name>",
|
|
44
|
+
"familyName": "<family name>",
|
|
45
45
|
"additionalName": null,
|
|
46
46
|
"address": null,
|
|
47
47
|
"birthDate": null,
|
|
48
|
-
"headline": "Founder,
|
|
48
|
+
"headline": "Founder, <company>",
|
|
49
49
|
"description": "Building {{productName}} …",
|
|
50
50
|
"industry": "Software",
|
|
51
51
|
"postalCode": null,
|
|
52
52
|
"addressLocality": "London, UK",
|
|
53
|
-
"twitterHandles": ["
|
|
54
|
-
"websites": ["https://
|
|
53
|
+
"twitterHandles": ["@<handle>"],
|
|
54
|
+
"websites": ["https://example.com"],
|
|
55
55
|
"instantMessengers": [],
|
|
56
56
|
"linkedinProfileUpdatedAt": "<ISO 8601 timestamp>",
|
|
57
57
|
"source": "linkedin"
|
|
@@ -15,8 +15,8 @@ The LLM step runs in the specialist's turn. There is no server-side Haiku round-
|
|
|
15
15
|
|
|
16
16
|
When the operator names a specific archive AND asks for insights, enrichment, or derived claims against it. Concrete triggers:
|
|
17
17
|
|
|
18
|
-
- "Derive insights from the
|
|
19
|
-
- "Enrich the
|
|
18
|
+
- "Derive insights from the WhatsApp archive for <contact>."
|
|
19
|
+
- "Enrich the <contact-a> × <contact-b> conversation archive — surface anything actionable."
|
|
20
20
|
- "Run Phase 2 on `:ConversationArchive elementId=4:abcd…:42`."
|
|
21
21
|
|
|
22
22
|
Never auto-fire after a Phase 1 ingest completes — `conversation-archive` (the ingest skill) explicitly writes ZERO observations. Phase 2 is operator-gated by design.
|
|
@@ -206,7 +206,7 @@ Re-ingesting the same `attachmentId` is safe — the writer drops prior `:Sectio
|
|
|
206
206
|
|
|
207
207
|
After `memory-ingest` returns the new KnowledgeDocument's `documentNodeId`, this step iterates the entities the dispatch brief named and connects each to the new document with the natural KD-level edge.
|
|
208
208
|
|
|
209
|
-
**Entity sources.** The dispatch brief's "key entities to connect" list. Brief shape: prose names of Persons, Organizations, Services, Tasks, Events, KnowledgeDocuments, BrandingData that the document describes or references. Example: *"Person nodes for
|
|
209
|
+
**Entity sources.** The dispatch brief's "key entities to connect" list. Brief shape: prose names of Persons, Organizations, Services, Tasks, Events, KnowledgeDocuments, BrandingData that the document describes or references. Example: *"Person nodes for each named individual; LocalBusiness / Organization nodes for each named company; any existing Task nodes related to the engagement the document describes."* Extract every named entity from the brief before any `memory-write`.
|
|
210
210
|
|
|
211
211
|
**Resolution.** For each named entity:
|
|
212
212
|
2. If the entity resolves to exactly one node, write the edge from KD to that node (see edge-type table below).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scheduling
|
|
3
3
|
description: "Calendar and scheduling — create events, manage appointments, set up recurring triggers."
|
|
4
|
-
account-owned-files: [{"file": "calendar-availability.json", "description": "The booking page's availability config
|
|
4
|
+
account-owned-files: [{"file": "calendar-availability.json", "writtenBy": "agent", "description": "The booking page's availability config. Authored by the agent under the calendar-site skill, which is also the only path that changes it; the publish-availability script reads it to refresh the live booking page."}]
|
|
5
5
|
tools:
|
|
6
6
|
- name: schedule-event
|
|
7
7
|
publicAllowlist: false
|
|
@@ -57,7 +57,7 @@ Dispatch the existing librarian / document-ingest pipeline against the rendered
|
|
|
57
57
|
- `mimeType = "text/markdown"`
|
|
58
58
|
|
|
59
59
|
The dispatch brief names:
|
|
60
|
-
- **Anchor:** `:AdminUser` (the confirmed owner from Phase 0).
|
|
60
|
+
- **Anchor:** `:AdminUser` (the confirmed owner from Phase 0). Every `:Person` resolved in Phase 0 appears in the **key entities to connect** list — `wire-brief-entities` writes the `MENTIONED` / `REPLIED_TO` / `QUOTED` edges from the new KD to each `:Person`.
|
|
61
61
|
- **`sourceUrl`:** `https://x.com/<handle>`
|
|
62
62
|
- **`sourceType`:** `'x'` (carried into `:KnowledgeDocument.source`)
|
|
63
63
|
- **`scope`:** `'admin'` (the tweet stream is the operator's authored content)
|
|
@@ -395,5 +395,31 @@ for _n in e-sign data-portal.json calendar-availability.json wa-channel-bindings
|
|
|
395
395
|
assert_grep "$_n" "$ALLOWED_SHIP" "ship-allow-$_n"
|
|
396
396
|
done
|
|
397
397
|
|
|
398
|
+
# --- declared-files fence: one name<TAB>plugin line per declared FILE -------
|
|
399
|
+
# The allowed-top-level fence is a layout list: it says a name may exist at the
|
|
400
|
+
# account root, not that an agent may author it. The write guard needs the file
|
|
401
|
+
# set apart from the bucket set, plus the owning plugin to name in its block
|
|
402
|
+
# message, so the merge emits it as its own fence.
|
|
403
|
+
DECL_SHIP="$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$A_SHIP/SCHEMA.md")"
|
|
404
|
+
for _n in wa-channel-bindings.json telegram-channel-bindings.json webchat-channel-bindings.json \
|
|
405
|
+
canonical-webchat-session.json session-titles.json agents-disabled.json; do
|
|
406
|
+
assert_grep "$(printf '%s\tadmin' "$_n")" "$DECL_SHIP" "ship-decl-$_n"
|
|
407
|
+
done
|
|
408
|
+
# An agent-written declaration (writtenBy: agent) stays OUT of the fence: the
|
|
409
|
+
# calendar-site and data-portal skills author these two files with Write, so
|
|
410
|
+
# denying them would break the only path that creates them. They remain in the
|
|
411
|
+
# allowed set (asserted above), so the reconcile still never calls them strays.
|
|
412
|
+
assert_nogrep "data-portal.json" "$DECL_SHIP" "ship-decl-excludes-agent-written-portal"
|
|
413
|
+
assert_nogrep "calendar-availability.json" "$DECL_SHIP" "ship-decl-excludes-agent-written-availability"
|
|
414
|
+
# A declared DIR never enters the file fence.
|
|
415
|
+
assert_nogrep "e-sign" "$DECL_SHIP" "ship-decl-excludes-dirs"
|
|
416
|
+
assert_grep "declared-files:start" "$(cat "$A_SHIP/SCHEMA.md")" "ship-decl-marker"
|
|
417
|
+
# Idempotent: a second merge leaves the file byte-identical.
|
|
418
|
+
B_SHIP="$(shasum "$A_SHIP/SCHEMA.md" | awk '{print $1}')"
|
|
419
|
+
merge_owned_dirs_into_schema "$A_SHIP" >/dev/null
|
|
420
|
+
assert_eq "$(shasum "$A_SHIP/SCHEMA.md" | awk '{print $1}')" "$B_SHIP" "ship-decl-idempotent"
|
|
421
|
+
# A brand whose payload declares no owned file emits no region at all.
|
|
422
|
+
assert_nogrep "declared-files:start" "$(cat "$A_MX/SCHEMA.md")" "mx-no-decl-region"
|
|
423
|
+
|
|
398
424
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
399
425
|
if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|
|
@@ -44,6 +44,12 @@ const ALLOWLIST = new Set([
|
|
|
44
44
|
// Same vi.hoisted pattern: Task 549's sidebar-sessions test pre-builds a
|
|
45
45
|
// tmpdir before the module-under-test's ACCOUNTS_DIR mock is wired.
|
|
46
46
|
'platform/ui/server/routes/admin/__tests__/sidebar-sessions.test.ts',
|
|
47
|
+
// Same vi.hoisted pattern: Task 2035's sidebar-sessions walk test pre-builds
|
|
48
|
+
// its projects tmpdir before the ACCOUNTS_DIR mock is wired.
|
|
49
|
+
'platform/ui/server/routes/admin/__tests__/sidebar-sessions-walk.test.ts',
|
|
50
|
+
// Same vi.hoisted pattern: Task 2035's reader title-cache test pre-builds its
|
|
51
|
+
// accounts tmpdir before the ACCOUNTS_DIR mock is wired.
|
|
52
|
+
'platform/ui/server/routes/__tests__/whatsapp-reader-title-cache.test.ts',
|
|
47
53
|
// Same vi.hoisted pattern: Task 684's sidebar-artefacts test pre-builds an
|
|
48
54
|
// empty tmpdir for ACCOUNTS_DIR/PLATFORM_ROOT before the module's mocks fire.
|
|
49
55
|
'platform/ui/server/routes/admin/__tests__/sidebar-artefacts.test.ts',
|
|
@@ -8,6 +8,13 @@ by kind:
|
|
|
8
8
|
account-owned-dirs: [{"dir": "quoting", "description": "..."}]
|
|
9
9
|
account-owned-files: [{"file": "data-portal.json", "description": "..."}]
|
|
10
10
|
|
|
11
|
+
A file entry may add "writtenBy": "agent" to say the agent authors it under a
|
|
12
|
+
skill's instruction rather than platform code writing it whole. Such a file is
|
|
13
|
+
still declared — the reconcile must not call it a stray — but it is left out of
|
|
14
|
+
the declared-files fence the write guard denies on, because denying it would
|
|
15
|
+
block the only path that creates it. Absent or any other value means platform
|
|
16
|
+
code is the writer.
|
|
17
|
+
|
|
11
18
|
The owned-entry set for a brand is the union declared by every platform plugin
|
|
12
19
|
($PROJECT_DIR/plugins/*/PLUGIN.md) and every premium sub-plugin of a bundle the
|
|
13
20
|
brand ships ($PREMIUM_ROOT/<bundle>/plugins/*/PLUGIN.md, gated by brand.json
|
|
@@ -30,6 +37,8 @@ MARK_START = "<!-- plugin-owned-dirs:start -->"
|
|
|
30
37
|
MARK_END = "<!-- plugin-owned-dirs:end -->"
|
|
31
38
|
ONT_START = "<!-- ontology-buckets:start -->"
|
|
32
39
|
ONT_END = "<!-- ontology-buckets:end -->"
|
|
40
|
+
DECL_START = "<!-- declared-files:start -->"
|
|
41
|
+
DECL_END = "<!-- declared-files:end -->"
|
|
33
42
|
|
|
34
43
|
# Base top-level names shipped in the account template plus `.quarantine`. This
|
|
35
44
|
# set is prune-protection only: a reconcile never *adds* a missing seed entry,
|
|
@@ -109,7 +118,9 @@ def _read_vertical(project_dir):
|
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
def _parse_plugin_md(path):
|
|
112
|
-
"""Return (name, [ {name, kind, description} ]) from a PLUGIN.md,
|
|
121
|
+
"""Return (name, [ {name, kind, description, writtenBy} ]) from a PLUGIN.md,
|
|
122
|
+
or (None, []). `writtenBy` is '' unless the entry states one; 'agent' keeps
|
|
123
|
+
the name out of the declared-files fence (see the module docstring).
|
|
113
124
|
Two frontmatter keys, one list: `account-owned-dirs` yields kind='dir',
|
|
114
125
|
`account-owned-files` yields kind='file'. A key whose value is not a JSON
|
|
115
126
|
array of objects is skipped WHOLE: entries are staged and committed only once
|
|
@@ -145,7 +156,8 @@ def _parse_plugin_md(path):
|
|
|
145
156
|
n = (e.get(field) or "").strip()
|
|
146
157
|
if n and "/" not in n:
|
|
147
158
|
staged.append({"name": n, "kind": kind,
|
|
148
|
-
"description": (e.get("description") or "").strip()
|
|
159
|
+
"description": (e.get("description") or "").strip(),
|
|
160
|
+
"writtenBy": (e.get("writtenBy") or "").strip()})
|
|
149
161
|
except Exception:
|
|
150
162
|
continue
|
|
151
163
|
owned.extend(staged)
|
|
@@ -190,7 +202,8 @@ def resolve(project_dir):
|
|
|
190
202
|
if e["name"] in seen:
|
|
191
203
|
continue
|
|
192
204
|
seen[e["name"]] = {"name": e["name"], "kind": e["kind"],
|
|
193
|
-
"description": e["description"], "plugin": name or "unknown"
|
|
205
|
+
"description": e["description"], "plugin": name or "unknown",
|
|
206
|
+
"writtenBy": e["writtenBy"]}
|
|
194
207
|
order.append(e["name"])
|
|
195
208
|
return [seen[n] for n in order]
|
|
196
209
|
|
|
@@ -355,9 +368,10 @@ def merge(project_dir, account_dir):
|
|
|
355
368
|
lines = text.split("\n")
|
|
356
369
|
lines[start + 1:end] = new_entries
|
|
357
370
|
text = "\n".join(lines)
|
|
358
|
-
# Regenerate
|
|
371
|
+
# Regenerate every generated region wholesale (idempotent).
|
|
359
372
|
text = _strip_region(text, MARK_START, MARK_END)
|
|
360
373
|
text = _strip_region(text, ONT_START, ONT_END)
|
|
374
|
+
text = _strip_region(text, DECL_START, DECL_END)
|
|
361
375
|
if owned:
|
|
362
376
|
region = [MARK_START, "## Plugin-owned top-level entries", "",
|
|
363
377
|
"A plugin declares the account-root entries its feature owns. A",
|
|
@@ -393,6 +407,28 @@ def merge(project_dir, account_dir):
|
|
|
393
407
|
f"(owned by `{b['owner']}`), not a root bucket.")
|
|
394
408
|
region.append(ONT_END)
|
|
395
409
|
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
410
|
+
# The declared-file set, machine-readable and separate from the
|
|
411
|
+
# allowed-top-level fence. That fence is a layout list — it says a name may
|
|
412
|
+
# exist at the account root — and the write guard reads it as its allow-list,
|
|
413
|
+
# so a name unioned into it becomes agent-writable. This fence carries the
|
|
414
|
+
# other half the guard needs: which of those names is written whole by
|
|
415
|
+
# platform code, and which plugin to name when it blocks the write. An
|
|
416
|
+
# agent-written declaration is excluded: its skill authors it with Write, so
|
|
417
|
+
# denying it would block the only path that creates it.
|
|
418
|
+
declared = [e for e in owned if e["kind"] == "file" and e["writtenBy"] != "agent"]
|
|
419
|
+
if declared:
|
|
420
|
+
region = [DECL_START, "## Declared files (written by platform code, owned by a plugin)", "",
|
|
421
|
+
"Each file below is written whole by platform code and owned by the",
|
|
422
|
+
"plugin named beside it. It is control-plane state, not operator data:",
|
|
423
|
+
"never hand-edit one, because the next platform write overwrites the",
|
|
424
|
+
"edit. The write guard reads this block and blocks a write whose first",
|
|
425
|
+
"path segment is one of these names.", "",
|
|
426
|
+
"```declared-files"]
|
|
427
|
+
for e in declared:
|
|
428
|
+
region.append(f"{e['name']}\t{e['plugin']}")
|
|
429
|
+
region.append("```")
|
|
430
|
+
region.append(DECL_END)
|
|
431
|
+
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
396
432
|
with open(schema_path, "w") as f:
|
|
397
433
|
f.write(text)
|
|
398
434
|
root_dirs = [b["dir"] for b in domain if _is_root(b["owner"])]
|