@rubytech/create-maxy-code 0.1.300 → 0.1.301
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/plugins/admin/PLUGIN.md +10 -10
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +84 -84
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +4 -4
- package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +4 -4
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +14 -8
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +3 -0
- package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
- package/payload/platform/plugins/docs/references/admin-identity-gate.md +6 -6
- package/payload/platform/plugins/docs/references/admin-session.md +15 -15
- package/payload/platform/plugins/docs/references/admin-ui.md +18 -18
- package/payload/platform/plugins/docs/references/aeo.md +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +3 -3
- package/payload/platform/plugins/docs/references/internals.md +28 -28
- package/payload/platform/plugins/docs/references/platform.md +21 -21
- package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
- package/payload/platform/plugins/docs/references/troubleshooting.md +6 -6
- package/payload/platform/plugins/docs/references/visitor-graph.md +3 -3
- package/payload/platform/plugins/email/PLUGIN.md +2 -2
- package/payload/platform/plugins/email/references/email-reference.md +2 -2
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +9 -9
- package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
- package/payload/platform/plugins/memory/references/schema-base.md +14 -14
- package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +1 -1
- package/payload/platform/plugins/memory/references/transcript-formats/otter.md +1 -1
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +6 -6
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +1 -1
- package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +8 -8
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +5 -5
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +1 -1
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -1
- package/payload/platform/plugins/slides/PROVENANCE.md +1 -1
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -1
- package/payload/platform/plugins/telegram/PLUGIN.md +1 -1
- package/payload/platform/plugins/url-get/PLUGIN.md +2 -2
- package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +5 -5
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +5 -5
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +1 -1
- package/payload/platform/plugins/workflows/PLUGIN.md +1 -1
- package/payload/platform/plugins/x-import/PLUGIN.md +2 -2
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
- package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +125 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +253 -0
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +54 -6
- package/payload/platform/scripts/resume-tunnel.sh +89 -5
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +14 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +11 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts +4 -0
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +54 -3
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +18 -0
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -0
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +137 -0
- package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
- package/payload/platform/templates/specialists/agents/librarian.md +2 -2
- package/payload/server/public/assets/{AdminShell-BFdBxg4P.js → AdminShell-BJrxifYa.js} +1 -1
- package/payload/server/public/assets/{Checkbox-BZ1XbA1t.js → Checkbox-C7bBRzLP.js} +1 -1
- package/payload/server/public/assets/{Transcript-CaVix6W0.js → Transcript-bGy_Gx99.js} +1 -1
- package/payload/server/public/assets/admin-jYKA7jR5.js +1 -0
- package/payload/server/public/assets/{audio-attachment-mime-CFyc3gpD.js → audio-attachment-mime-DL5R1M2A.js} +1 -1
- package/payload/server/public/assets/{browser-ClnGUKp-.js → browser-7fyPCFWB.js} +1 -1
- package/payload/server/public/assets/{chat-ZTAhSoJV.js → chat-UF9ATDwe.js} +1 -1
- package/payload/server/public/assets/{data-DDjeQQ7e.js → data-CIJnkfgX.js} +1 -1
- package/payload/server/public/assets/{file-download-DBMW0BNz.js → file-download-gth6Gpmn.js} +1 -1
- package/payload/server/public/assets/{graph-CR74qec3.js → graph-DHnB3BHK.js} +1 -1
- package/payload/server/public/assets/{graph-labels-1Uz0ZBRd.js → graph-labels-huk-fKcY.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-DZddXDvJ.css +1 -0
- package/payload/server/public/assets/operator-D3QAzlPs.js +1 -0
- package/payload/server/public/assets/page-86edaOS1.js +1 -0
- package/payload/server/public/assets/{public-W-HholkU.js → public-B4LqL6lA.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-geYq13zs.js → useSelectionMode-lrH0qwAR.js} +1 -1
- package/payload/server/public/browser.html +6 -6
- package/payload/server/public/chat.html +8 -8
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +8 -8
- package/payload/server/public/index.html +9 -9
- package/payload/server/public/operator.html +9 -9
- package/payload/server/public/public.html +6 -6
- package/payload/server/server.js +51 -6
- package/payload/server/public/assets/admin-BK7xIr3o.js +0 -1
- package/payload/server/public/assets/jsx-runtime-BqGVUT1h.css +0 -1
- package/payload/server/public/assets/operator-QP_z97s8.js +0 -1
- package/payload/server/public/assets/page-CLijftr1.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-D3JkFIbi.js → jsx-runtime-CMu8uN0-.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp",
|
|
3
|
-
"description": "WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send
|
|
3
|
+
"description": "WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send); observation-only in groups (the agent never responds in a group).",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: whatsapp
|
|
3
|
-
description: WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send
|
|
3
|
+
description: WhatsApp messaging channel — Baileys QR pairing, DM policy enforcement, inbound routing, business hours gating, conversation browsing. Reply-only in DMs (no agent-initiated send); observation-only in groups (the agent never responds in a group).
|
|
4
4
|
tools:
|
|
5
5
|
- name: whatsapp-login-start
|
|
6
6
|
publicAllowlist: false
|
|
@@ -57,7 +57,7 @@ Activate when the user asks about WhatsApp — connecting, linking, checking sta
|
|
|
57
57
|
- QR-code-based WhatsApp pairing via Baileys (linked device) — `whatsapp-login-start` returns the QR as a scannable Unicode block printed on the PTY surface (the only operator surface); the operator scans it directly from the terminal output
|
|
58
58
|
- Multi-account support (optional, multiple linked numbers)
|
|
59
59
|
- Inbound message routing — sender-keyed `claude` PTY sessions via the loopback claude-session-manager (admin phone → admin agent, public → resolved public agent); a "typing…" presence is sent on receipt as an ack while the agent turn runs
|
|
60
|
-
- Group observation — group messages are stored, persisted, and browsable, but the agent never responds in a group (observation-only
|
|
60
|
+
- Group observation — group messages are stored, persisted, and browsable, but the agent never responds in a group (observation-only)
|
|
61
61
|
- DM policy enforcement (open, allowlist, disabled)
|
|
62
62
|
- Conversation browsing — list active conversations, read message history, inspect group metadata
|
|
63
63
|
- Inbound voice note transcription (OGG Opus → text via local whisper.cpp)
|
|
@@ -76,17 +76,17 @@ Four tools for reading WhatsApp conversation context. The first two read from th
|
|
|
76
76
|
| `whatsapp-conversation-graph-state` | Persisted `:Message:WhatsAppMessage` rows attached to the `:Conversation` (sessionKey-keyed) plus the resolved `sessionId`, the cypher string, and a divergence header reporting graph-row count vs in-memory count. Use when the question is "what got persisted" — never compose the cypher manually |
|
|
77
77
|
| `whatsapp-group-info` | Group metadata from WhatsApp servers — subject, description, participants with admin flags, creation date. Requires active connection |
|
|
78
78
|
|
|
79
|
-
## Group access — observation-only
|
|
79
|
+
## Group access — observation-only
|
|
80
80
|
|
|
81
81
|
The agent never responds in a group. `checkGroupAccess` ([`inbound/access-control.ts`](../../ui/app/lib/whatsapp/inbound/access-control.ts)) is a structural observe-only deny — it always returns `allowed:false reason:group-observe-only`, so every group inbound ends at the manager's `!accessResult.allowed` return before dispatch. There is no group policy, allowlist, per-group activation, or @mention setting; setting any of those keys is a `.strict()` schema rejection, not a silently-dead key. A belt invariant in the manager's `reply` closure refuses any `@g.us` recipient and logs `op=group-reply-blocked` — that line is the leak signature if a dispatch path ever leaks.
|
|
82
82
|
|
|
83
83
|
Observation is fully preserved: group messages are stored in the ring buffer, recorded as activity, and persisted to the graph at the access-independent capture site, and remain visible via `whatsapp-conversations`, `whatsapp-group-info`, `list-groups`, and the `/whatsapp` reader. This is a Meta-policy constraint (automated messaging from an unofficial linked device is penalised, the same class as the reply-only DM posture); for agent-driven group messaging, Telegram is the recommended channel.
|
|
84
84
|
|
|
85
|
-
## Native channel reply surface
|
|
85
|
+
## Native channel reply surface
|
|
86
86
|
|
|
87
87
|
A native admin channel session runs a per-sender `whatsapp-channel` MCP server (`platform/services/whatsapp-channel/`) exposing two reply-only tools: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to the gateway's `POST /wa-channel/reply-document`, which funnels through `sendWhatsAppDocument` — the single send core that owns account-dir path validation, the 100 MB ceiling, the canonical `[whatsapp:outbound] sent document …` log, and per-file reconciliation (`op=reply-document-reconciled` on delivery, `file-delivery-unreconciled` on a miss — no silent failure). A bare `SendUserFile` is also caught and delivered through the same core by a read-only JSONL follower on the session, so an attachment reaches the sender even when the agent skips the explicit tool. Both surfaces are reply-only: a file reply to a sender with no live conversation is refused. See [channels-whatsapp.md](references/channels-whatsapp.md) and `.docs/whatsapp-inbound-lifeline.md`.
|
|
88
88
|
|
|
89
|
-
## Self-chat command lane
|
|
89
|
+
## Self-chat command lane
|
|
90
90
|
|
|
91
91
|
The agent links as a companion device on the operator's own WhatsApp account, so the operator cannot DM the agent from the paired handset — those messages arrive as `fromMe=true` / upsert type `append`. The operator's **self-chat** (message-yourself thread) is the command lane from that handset: the `append` branch in `manager.ts` evaluates a six-clause discriminator (`inbound/self-chat.ts`) — fromMe, self-surface jid (selfJid/selfLid/`selfPhone@s.whatsapp.net`, device-suffix normalised), not an agent-sent echo, timestamp strictly after `lastConnectedAt` (excludes reconnect history backfill; restart-safe backstop for the 2-min echo cache), not a duplicate, non-empty text — and dispatches a match to the **admin** agent with raw text; the reply lands back in the self-chat. Every self-chat `fromMe` message emits one `[whatsapp:self-chat] op=seen` line followed by `op=dispatch` or `op=skip reason=echo|backfill|duplicate|no-text`, so a dropped command is never silent. `fromMe` in any other thread is unchanged (`[Owner reply]` mirror). See [channels-whatsapp.md](references/channels-whatsapp.md) §Self-chat command lane.
|
|
92
92
|
|
|
@@ -6,13 +6,13 @@ WhatsApp connects via Baileys multi-account:
|
|
|
6
6
|
|----------|----------|------------|
|
|
7
7
|
| **baileys** | All WhatsApp accounts | WebSocket via QR code (linked device) |
|
|
8
8
|
|
|
9
|
-
## Reply-only posture
|
|
9
|
+
## Reply-only posture
|
|
10
10
|
|
|
11
11
|
The agent has **no tool to initiate a WhatsApp message to an arbitrary number.** The `whatsapp-send` and `whatsapp-send-document` MCP tools were removed after an agent-initiated cold first-contact send (to a number with no prior conversation) tripped WhatsApp's anti-automation guard and restricted the operator's account. Initiating is intentionally unavailable, not merely gated — a soft approval gate already existed and did not prevent the incident.
|
|
12
12
|
|
|
13
|
-
What the agent **can** still do: reply within an existing chat. On a native admin channel session the whatsapp-channel server (`platform/services/whatsapp-channel/`) exposes two reply tools, both reply-only: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to `POST /wa-channel/reply-document` → `sendWhatsAppDocument`, the one send core that owns path validation, the 100 MB ceiling, the canonical log, and per-file reconciliation
|
|
13
|
+
What the agent **can** still do: reply within an existing chat. On a native admin channel session the whatsapp-channel server (`platform/services/whatsapp-channel/`) exposes two reply tools, both reply-only: `reply` (text) and `reply-document` (one or more files delivered as WhatsApp documents). `reply-document` POSTs to `POST /wa-channel/reply-document` → `sendWhatsAppDocument`, the one send core that owns path validation, the 100 MB ceiling, the canonical log, and per-file reconciliation. A bare `SendUserFile` is also caught and delivered through the same core by a read-only JSONL follower on the session, so an attachment reaches the sender even if the agent skips the explicit tool. File delivery is refused for a sender with no live conversation — same reply-only posture as text. (The removed `whatsapp-send-document` route tool and the bridge path `SendUserFile` → `file-delivery-bridge.ts` → `POST /api/whatsapp/send-document` are the legacy webchat/email bridge surface, not the native WhatsApp channel.) Replying within your chats is what WhatsApp permits; starting new chats from a linked device is what it penalises.
|
|
14
14
|
|
|
15
|
-
## Observation-only in groups
|
|
15
|
+
## Observation-only in groups
|
|
16
16
|
|
|
17
17
|
The agent **never responds in a group chat.** Every inbound group message is stored, recorded, and persisted to the graph (observation is fully preserved — `list-groups`, `whatsapp-group-info`, conversations, and the `/whatsapp` reader all still work), but it is never dispatched to a responding session and never produces an outbound message into the group. `checkGroupAccess` is a structural observe-only deny — there is no group policy, allowlist, per-group activation, or @mention setting to turn responses on. A belt invariant in the manager's `reply` closure refuses any `@g.us` recipient (`op=group-reply-blocked`).
|
|
18
18
|
|
|
@@ -118,7 +118,7 @@ Admin phones always bypass DM policy — the `isAdminPhone()` check runs before
|
|
|
118
118
|
|
|
119
119
|
**Schema constraint:** `dmPolicy: "open"` requires `allowFrom` to include `"*"` (enforced by Zod validation).
|
|
120
120
|
|
|
121
|
-
**Key file:** `access-control.ts` — `checkDmAccess()` gates DMs by the policy above; `checkGroupAccess()` is a structural observe-only deny (always `allowed:false reason:group-observe-only
|
|
121
|
+
**Key file:** `access-control.ts` — `checkDmAccess()` gates DMs by the policy above; `checkGroupAccess()` is a structural observe-only deny (always `allowed:false reason:group-observe-only`) — no group policy is consulted.
|
|
122
122
|
|
|
123
123
|
**Config path:** Per-account at `whatsapp.accounts.{accountId}.dmPolicy` in `account.json`. Falls back to `whatsapp.dmPolicy`. Changes are conversational — the admin agent writes to config via `whatsapp-config action: "update-config"`.
|
|
124
124
|
|
|
@@ -150,7 +150,7 @@ The dedupe cache (`agentSentMessages`) has a 2-minute TTL and 500-entry cap, whi
|
|
|
150
150
|
| `src/web/inbound/dedupe.ts` | `trackAgentSentMessage` / `isAgentSentMessage` — echo suppression cache |
|
|
151
151
|
| `src/infra/outbound/cross-agent-echo.ts` | Adapter: agent tool → `mirrorOwnerReplyToSession` |
|
|
152
152
|
|
|
153
|
-
## Self-chat command lane
|
|
153
|
+
## Self-chat command lane
|
|
154
154
|
|
|
155
155
|
The companion-device link means the operator's paired phone cannot DM the agent — a message typed from the paired number arrives as `fromMe=true` with upsert type `append`, which the manager's append branch historically dropped before dispatch. The operator's **self-chat** (the message-yourself thread, `remoteJid` == the account's own jid/lid) is the exception: the agent never writes there first, so it is an unambiguous admin command surface.
|
|
156
156
|
|
|
@@ -70,7 +70,7 @@ The form's `submitMessage` must map the field values back to `update-config` fie
|
|
|
70
70
|
|
|
71
71
|
The agent is **observation-only in groups**: it sees and records every group message but never replies in a group. There is no per-group setting to turn responses on — the restriction is structural, not a toggle. `whatsapp-config action: list-groups` still lists the groups the connected phone is in, for reference.
|
|
72
72
|
|
|
73
|
-
This is a **Meta-policy constraint, not a Maxy limitation.** WhatsApp penalises automated and business-style messaging sent from an unofficial linked device. Replying in groups and sending cold first-contact messages are both anti-automation triggers — they restricted a live operator account, which is why the agent is observe-only in groups and reply-only in DMs
|
|
73
|
+
This is a **Meta-policy constraint, not a Maxy limitation.** WhatsApp penalises automated and business-style messaging sent from an unofficial linked device. Replying in groups and sending cold first-contact messages are both anti-automation triggers — they restricted a live operator account, which is why the agent is observe-only in groups and reply-only in DMs.
|
|
74
74
|
|
|
75
75
|
If the user wants an agent that actively messages in groups or initiates conversations, recommend **Telegram** instead. Telegram has no equivalent linked-device automation penalty, so the agent can send and respond freely there. Point them to the Telegram setup path at `platform/plugins/telegram/references/setup-guide.md`.
|
|
76
76
|
|
|
@@ -124,7 +124,7 @@ Each step declares an `onFailure` policy:
|
|
|
124
124
|
| `skipped` | Step failed but `onFailure: "skip"` kept execution going |
|
|
125
125
|
| `failed` | Step failed and `onFailure: "abort"` stopped execution |
|
|
126
126
|
|
|
127
|
-
(Historic `degraded` step results from
|
|
127
|
+
(Historic `degraded` step results from an earlier runner version remain in the graph; the runner no longer produces them.)
|
|
128
128
|
|
|
129
129
|
## Execution History
|
|
130
130
|
|
|
@@ -21,7 +21,7 @@ The admin agent delegates to `specialists:database-operator` when the operator d
|
|
|
21
21
|
|
|
22
22
|
## What this plugin is not
|
|
23
23
|
|
|
24
|
-
- **Not a new graph writer.** Every write routes through the existing `memory-ingest` surface — the document path keyed on `attachmentId` (tweet stream) writes `:KnowledgeDocument`, or the conversation-archive path keyed on `conversationIdentity` (DMs) writes `:ConversationArchive
|
|
24
|
+
- **Not a new graph writer.** Every write routes through the existing `memory-ingest` surface — the document path keyed on `attachmentId` (tweet stream) writes `:KnowledgeDocument`, or the conversation-archive path keyed on `conversationIdentity` (DMs) writes `:ConversationArchive`. Both attach `:Section` children. No bespoke `:Post`, `:DirectMessage`, `:DMThread`, `:Attachment`, or `:Thread` label is introduced; per-thread KD granularity is explicitly rejected.
|
|
25
25
|
- **Not a live X API client.** Archive-only — the X API is rate-limited and paid.
|
|
26
26
|
- **Not a retweet importer.** Retweets-without-comment are not persisted reliably in X archives; the skill drops them.
|
|
27
27
|
|
|
@@ -29,5 +29,5 @@ The admin agent delegates to `specialists:database-operator` when the operator d
|
|
|
29
29
|
|
|
30
30
|
- **memory** — the underlying graph-write surface used by the skill (`mcp__plugin_memory_memory__memory-write` etc.). The skill passes `source='x'` / `source='x-dm'` and `createdByAgent='x-import'` for provenance on every write.
|
|
31
31
|
- **document-ingest** (memory skill) — owns the tweet-stream KnowledgeDocument writes. `:Section` classification is its responsibility, not this plugin's.
|
|
32
|
-
- **conversation-archive** (memory skill) — owns the DM transcript writes (`:ConversationArchive` keyed on `conversationIdentity`
|
|
32
|
+
- **conversation-archive** (memory skill) — owns the DM transcript writes (`:ConversationArchive` keyed on `conversationIdentity`). The `x-dm` enum entry on `CONVERSATION_SOURCES` and the sibling normaliser in `platform/plugins/memory/mcp/src/lib/conversation-normalisers/x-dm.ts` are the only new code outside this plugin.
|
|
33
33
|
- **database-operator specialist** — owns execution. See [platform/templates/specialists/agents/database-operator.md](../../templates/specialists/agents/database-operator.md) external-archive ingestion clause.
|
|
@@ -118,6 +118,6 @@ Every `.js` shim read by this skill is gated through the wrapper regex documente
|
|
|
118
118
|
|
|
119
119
|
- **Per-thread KDs.** One archive = one tweet-stream KD. Thread boundaries are section boundaries the classifier owns, not graph objects.
|
|
120
120
|
- **Bespoke labels.** No `:Post`, `:Tweet`, `:DirectMessage`, `:DMThread`, `:Attachment`. Everything maps to the existing `:KnowledgeDocument` + `:ConversationArchive` + `:ImageObject` shapes.
|
|
121
|
-
- **Tweet-level typed edges.** `:REPLIES_TO` / `:QUOTES` as graph edges off individual tweets are rejected; document-level `REPLIED_TO` / `QUOTED` from the KD to each `:Person` covers the same traversals at the right granularity. The
|
|
121
|
+
- **Tweet-level typed edges.** `:REPLIES_TO` / `:QUOTES` as graph edges off individual tweets are rejected; document-level `REPLIED_TO` / `QUOTED` from the KD to each `:Person` covers the same traversals at the right granularity. The typed-edge pass enumerates the new KD and writes any further `COMMITTED_TO` / `MENTIONS` / `REFERENCES` edges from in-body references.
|
|
122
122
|
- **Retweets-without-comment.** Not persisted reliably in X archives; dropped at parse.
|
|
123
123
|
- **Live X API.** Archive-only.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Standing guard for the markdown task-id leak scan. If a fixture citation under
|
|
3
|
+
# a shipped plugin/template .md stops failing the gate, the markdown coverage
|
|
4
|
+
# regressed back to code-only and internal task numbers can ship to clients
|
|
5
|
+
# again. The gate reads its scan root from TASK_ID_LEAK_ROOT so this test points
|
|
6
|
+
# it at a mktemp fixture tree, never the real corpus.
|
|
7
|
+
set -u
|
|
8
|
+
GATE="$(cd "$(dirname "$0")/.." && pwd)/check-no-task-id-leaks.mjs"
|
|
9
|
+
[[ -f "$GATE" ]] || { echo "FAIL: $GATE missing" >&2; exit 1; }
|
|
10
|
+
PASS=0; FAIL=0
|
|
11
|
+
|
|
12
|
+
ROOT=$(mktemp -d)
|
|
13
|
+
trap 'rm -rf "$ROOT"' EXIT
|
|
14
|
+
mkdir -p "$ROOT/platform/plugins/demo/skills/demo" \
|
|
15
|
+
"$ROOT/platform/templates/specialists" \
|
|
16
|
+
"$ROOT/.docs"
|
|
17
|
+
|
|
18
|
+
clean_skill() { printf '# Demo skill\n\nDoes a thing.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"; }
|
|
19
|
+
clean_skill
|
|
20
|
+
|
|
21
|
+
# 1. Clean corpus -> gate passes.
|
|
22
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
23
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
24
|
+
echo "PASS: clean markdown corpus -> exit 0"; PASS=$((PASS+1))
|
|
25
|
+
else
|
|
26
|
+
echo "FAIL: clean corpus tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 2. `(Task 999)` parenthetical under a shipped skill -> gate fails + names it.
|
|
30
|
+
printf '# Demo skill\n\nDoes a thing (Task 999).\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
31
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
32
|
+
if [[ "$rc" -ne 0 ]] \
|
|
33
|
+
&& printf '%s' "$out" | grep -q '\[task-id-leak\] file=platform/plugins/demo/skills/demo/SKILL.md'; then
|
|
34
|
+
echo "PASS: (Task 999) in shipped skill fails the gate"; PASS=$((PASS+1))
|
|
35
|
+
else
|
|
36
|
+
echo "FAIL: parenthetical citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
37
|
+
fi
|
|
38
|
+
clean_skill
|
|
39
|
+
|
|
40
|
+
# 3. Bare `Task 999` (prose form) under a shipped template -> gate fails + names it.
|
|
41
|
+
printf 'Task 999 changed the routing.\n' > "$ROOT/platform/templates/specialists/AGENT.md"
|
|
42
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
43
|
+
if [[ "$rc" -ne 0 ]] \
|
|
44
|
+
&& printf '%s' "$out" | grep -q '\[task-id-leak\] file=platform/templates/specialists/AGENT.md'; then
|
|
45
|
+
echo "PASS: bare Task 999 in shipped template fails the gate"; PASS=$((PASS+1))
|
|
46
|
+
else
|
|
47
|
+
echo "FAIL: bare prose citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
48
|
+
fi
|
|
49
|
+
rm -f "$ROOT/platform/templates/specialists/AGENT.md"
|
|
50
|
+
|
|
51
|
+
# 4. `Tasks 12 and 34` plural form under a shipped skill -> gate fails.
|
|
52
|
+
printf '# Demo skill\n\nMerged behaviour (Tasks 12 and 34).\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
53
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
54
|
+
if [[ "$rc" -ne 0 ]] \
|
|
55
|
+
&& printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=Tasks 12'; then
|
|
56
|
+
echo "PASS: plural Tasks NNN form fails the gate"; PASS=$((PASS+1))
|
|
57
|
+
else
|
|
58
|
+
echo "FAIL: plural citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
59
|
+
fi
|
|
60
|
+
clean_skill
|
|
61
|
+
|
|
62
|
+
# 4b. Hyphenated `Pre-Task-748` form under a shipped skill -> gate fails.
|
|
63
|
+
printf '# Demo skill\n\nPre-Task-748 the index was small.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
64
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
65
|
+
if [[ "$rc" -ne 0 ]] \
|
|
66
|
+
&& printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=Task-748'; then
|
|
67
|
+
echo "PASS: hyphenated Pre-Task-748 form fails the gate"; PASS=$((PASS+1))
|
|
68
|
+
else
|
|
69
|
+
echo "FAIL: hyphenated citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
70
|
+
fi
|
|
71
|
+
clean_skill
|
|
72
|
+
|
|
73
|
+
# 4c. A lowercase `task-836-foo` slug (branch/file name) -> gate does NOT trip.
|
|
74
|
+
printf '# Demo skill\n\nWork landed on branch task-836-foo.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
75
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
76
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
77
|
+
echo "PASS: lowercase task-836 slug is not flagged"; PASS=$((PASS+1))
|
|
78
|
+
else
|
|
79
|
+
echo "FAIL: lowercase slug wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
80
|
+
fi
|
|
81
|
+
clean_skill
|
|
82
|
+
|
|
83
|
+
# 4d. Lowercase prose `before task 127` under a shipped skill -> gate fails.
|
|
84
|
+
printf '# Demo skill\n\nResults from before task 127 remain in the graph.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
85
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
86
|
+
if [[ "$rc" -ne 0 ]] \
|
|
87
|
+
&& printf '%s' "$out" | grep -q '\[task-id-leak\] .*match=task 127'; then
|
|
88
|
+
echo "PASS: lowercase prose 'task 127' fails the gate"; PASS=$((PASS+1))
|
|
89
|
+
else
|
|
90
|
+
echo "FAIL: lowercase prose citation not caught (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
91
|
+
fi
|
|
92
|
+
clean_skill
|
|
93
|
+
|
|
94
|
+
# 4e. A `.tasks/<n>-…-task-NNN.md` file-path reference -> gate does NOT trip
|
|
95
|
+
# (file paths are out of scope; the hyphenated-lowercase shape is exempt).
|
|
96
|
+
printf '# Demo skill\n\nTracked in `.tasks/339-installer-smoke-task-297.md` for now.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
97
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
98
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
99
|
+
echo "PASS: .tasks/ file-path reference is not flagged"; PASS=$((PASS+1))
|
|
100
|
+
else
|
|
101
|
+
echo "FAIL: .tasks/ path wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
102
|
+
fi
|
|
103
|
+
clean_skill
|
|
104
|
+
|
|
105
|
+
# 5. A hex colour `#999` in a shipped .md -> gate does NOT trip (not a citation).
|
|
106
|
+
printf '# Demo skill\n\nUse colour `#999` for labels.\n' > "$ROOT/platform/plugins/demo/skills/demo/SKILL.md"
|
|
107
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
108
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
109
|
+
echo "PASS: hex colour #999 is not flagged as a citation"; PASS=$((PASS+1))
|
|
110
|
+
else
|
|
111
|
+
echo "FAIL: hex colour wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
112
|
+
fi
|
|
113
|
+
clean_skill
|
|
114
|
+
|
|
115
|
+
# 6. Citation in a dev-only .docs doc -> gate does NOT trip (not a shipped root).
|
|
116
|
+
printf 'Internal note about Task 999.\n' > "$ROOT/.docs/note.md"
|
|
117
|
+
out=$(TASK_ID_LEAK_ROOT="$ROOT" node "$GATE" 2>&1); rc=$?
|
|
118
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
119
|
+
echo "PASS: .docs citation is exempt (dev-only, unscanned)"; PASS=$((PASS+1))
|
|
120
|
+
else
|
|
121
|
+
echo "FAIL: .docs citation wrongly tripped gate (rc=$rc): $out" >&2; FAIL=$((FAIL+1))
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo "----"; echo "PASS=$PASS FAIL=$FAIL"
|
|
125
|
+
[[ "$FAIL" -eq 0 ]]
|
|
@@ -276,12 +276,265 @@ case_supervision_contract() {
|
|
|
276
276
|
return 0
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Task 833 — stray-connector reap.
|
|
281
|
+
#
|
|
282
|
+
# install_reap_stubs adds a `ps` stub and a `kill` recorder on top of setup().
|
|
283
|
+
# The ps stub reads a table file ($root/ps_table) of "<pid> <argv...>" lines and
|
|
284
|
+
# answers both query shapes the script uses: `-eo pid=,args=` (full table) and
|
|
285
|
+
# `-p <pid> -o args=` (single argv). The recorder ($root/bin/killrec, wired via
|
|
286
|
+
# RESUME_TUNNEL_KILL_CMD) appends -TERM/-KILL calls to $root/reap_log; on a `-0`
|
|
287
|
+
# liveness query it reports "gone" (exit 1) unless $root/reap_alive exists.
|
|
288
|
+
install_reap_stubs() {
|
|
289
|
+
local root="$1"
|
|
290
|
+
local bin_dir="$root/bin"
|
|
291
|
+
|
|
292
|
+
cat > "$bin_dir/ps" <<SH
|
|
293
|
+
#!/bin/bash
|
|
294
|
+
table="$root/ps_table"
|
|
295
|
+
if [[ "\$*" == *"-p"* ]]; then
|
|
296
|
+
pid=""; prev=""
|
|
297
|
+
for a in "\$@"; do
|
|
298
|
+
[[ "\$prev" == "-p" ]] && pid="\$a"
|
|
299
|
+
prev="\$a"
|
|
300
|
+
done
|
|
301
|
+
awk -v want="\$pid" '{ if (\$1==want){ \$1=""; sub(/^ /,""); print } }' "\$table" 2>/dev/null
|
|
302
|
+
exit 0
|
|
303
|
+
fi
|
|
304
|
+
cat "\$table" 2>/dev/null
|
|
305
|
+
exit 0
|
|
306
|
+
SH
|
|
307
|
+
chmod +x "$bin_dir/ps"
|
|
308
|
+
|
|
309
|
+
cat > "$bin_dir/killrec" <<SH
|
|
310
|
+
#!/bin/bash
|
|
311
|
+
if [[ "\$1" == "-0" ]]; then
|
|
312
|
+
[[ -f "$root/reap_alive" ]] && exit 0 || exit 1
|
|
313
|
+
fi
|
|
314
|
+
echo "\$*" >> "$root/reap_log"
|
|
315
|
+
exit 0
|
|
316
|
+
SH
|
|
317
|
+
chmod +x "$bin_dir/killrec"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Write a config.yml carrying the brand tunnel id so the audit line can resolve it.
|
|
321
|
+
write_branded_config() {
|
|
322
|
+
local cfg="$1"
|
|
323
|
+
cat > "$cfg" <<'YML'
|
|
324
|
+
tunnel: test-tunnel-id
|
|
325
|
+
credentials-file: /dev/null
|
|
326
|
+
ingress:
|
|
327
|
+
- service: http_status:404
|
|
328
|
+
YML
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Run resume-tunnel with the reap stubs + recorder active.
|
|
332
|
+
# Args: root config_dir [extra env assignments...]
|
|
333
|
+
run_with_reap() {
|
|
334
|
+
local root="$1" config_dir="$2"; shift 2
|
|
335
|
+
# Use env so any extra "NAME=VALUE" args (e.g. RESUME_TUNNEL_REAP_GRACE=0)
|
|
336
|
+
# are applied as assignments — a variable-expanded NAME=VALUE word is NOT
|
|
337
|
+
# treated as an assignment by bash's own simple-command parsing.
|
|
338
|
+
env \
|
|
339
|
+
HOME="$root/home" \
|
|
340
|
+
PATH="$root/bin:$PATH" \
|
|
341
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
342
|
+
RESUME_TUNNEL_KILL_CMD="$root/bin/killrec" \
|
|
343
|
+
RESUME_TUNNEL_VERIFY_DEADLINE=2 \
|
|
344
|
+
"$@" \
|
|
345
|
+
bash "$RESUME"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# CASE A: stray present, service inactive → reap kills stray, leaves none, then spawns.
|
|
349
|
+
case_reap_stray_then_spawn() {
|
|
350
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
351
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
352
|
+
install_reap_stubs "$root"
|
|
353
|
+
local home="$root/home"
|
|
354
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
355
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
356
|
+
write_branded_config "$cfg"
|
|
357
|
+
cat > "$root/ps_table" <<TBL
|
|
358
|
+
210086 cloudflared --origincert $cert --config $cfg tunnel run
|
|
359
|
+
777 cloudflared --origincert /home/other/.realagent/cloudflared/cert.pem --config /home/other/.realagent/cloudflared/config.yml tunnel run
|
|
360
|
+
TBL
|
|
361
|
+
|
|
362
|
+
run_with_reap "$root" ".maxy-code"
|
|
363
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
364
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
365
|
+
cleanup "$root"
|
|
366
|
+
|
|
367
|
+
echo "$log" | grep -q "op=reap brand=maxy-code found=1 killed=1 survivor=none" || { echo " census wrong"; echo "$log"; return 1; }
|
|
368
|
+
echo "$rec" | grep -q -- "-TERM 210086" || { echo " stray not SIGTERMed"; echo "$rec"; return 1; }
|
|
369
|
+
echo "$rec" | grep -q "777" && { echo " co-resident 777 was signalled"; echo "$rec"; return 1; }
|
|
370
|
+
echo "$log" | grep -q "spawned service=cloudflared-maxy-code.service" || { echo " did not spawn after reap"; echo "$log"; return 1; }
|
|
371
|
+
return 0
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# CASE B: supervised survivor + stray, service active → reap kills only stray, no spawn.
|
|
375
|
+
case_reap_keeps_survivor() {
|
|
376
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
377
|
+
setup "$root" ".maxy-code" "active" "yes"
|
|
378
|
+
install_reap_stubs "$root"
|
|
379
|
+
local home="$root/home"
|
|
380
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
381
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
382
|
+
write_branded_config "$cfg"
|
|
383
|
+
# 12345 is the MainPID the systemctl stub reports.
|
|
384
|
+
cat > "$root/ps_table" <<TBL
|
|
385
|
+
12345 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
|
|
386
|
+
210086 cloudflared --origincert $cert --config $cfg tunnel run
|
|
387
|
+
TBL
|
|
388
|
+
|
|
389
|
+
run_with_reap "$root" ".maxy-code"
|
|
390
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
391
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
392
|
+
cleanup "$root"
|
|
393
|
+
|
|
394
|
+
echo "$log" | grep -q "op=reap brand=maxy-code found=2 killed=1 survivor=12345" || { echo " census wrong"; echo "$log"; return 1; }
|
|
395
|
+
echo "$rec" | grep -q -- "-TERM 210086" || { echo " stray not SIGTERMed"; echo "$rec"; return 1; }
|
|
396
|
+
echo "$rec" | grep -q "12345" && { echo " survivor 12345 was signalled"; echo "$rec"; return 1; }
|
|
397
|
+
echo "$log" | grep -q "spawned service=" && { echo " spawned while already active"; echo "$log"; return 1; }
|
|
398
|
+
echo "$log" | grep -q "already running" || { echo " missing already-running log"; echo "$log"; return 1; }
|
|
399
|
+
return 0
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# CASE C: no stray, service active → idempotent, no kill, no spawn.
|
|
403
|
+
case_reap_idempotent() {
|
|
404
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
405
|
+
setup "$root" ".maxy-code" "active" "yes"
|
|
406
|
+
install_reap_stubs "$root"
|
|
407
|
+
local home="$root/home"
|
|
408
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
409
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
410
|
+
write_branded_config "$cfg"
|
|
411
|
+
cat > "$root/ps_table" <<TBL
|
|
412
|
+
12345 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
|
|
413
|
+
TBL
|
|
414
|
+
|
|
415
|
+
run_with_reap "$root" ".maxy-code"
|
|
416
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
417
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
418
|
+
cleanup "$root"
|
|
419
|
+
|
|
420
|
+
echo "$log" | grep -q "op=reap brand=maxy-code found=1 killed=0 survivor=12345" || { echo " census wrong"; echo "$log"; return 1; }
|
|
421
|
+
[ -z "$rec" ] || { echo " recorder not empty"; echo "$rec"; return 1; }
|
|
422
|
+
echo "$log" | grep -q "spawned service=" && { echo " spawned while single+active"; echo "$log"; return 1; }
|
|
423
|
+
return 0
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# CASE D: audit line resolves the brand tunnel id from the victim's --config.
|
|
427
|
+
case_reap_audit_tunnel_id() {
|
|
428
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
429
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
430
|
+
install_reap_stubs "$root"
|
|
431
|
+
local home="$root/home"
|
|
432
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
433
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
434
|
+
write_branded_config "$cfg"
|
|
435
|
+
cat > "$root/ps_table" <<TBL
|
|
436
|
+
210086 cloudflared --origincert $cert --config $cfg tunnel run
|
|
437
|
+
TBL
|
|
438
|
+
|
|
439
|
+
run_with_reap "$root" ".maxy-code"
|
|
440
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
441
|
+
cleanup "$root"
|
|
442
|
+
|
|
443
|
+
echo "$log" | grep -q "op=reap brand=maxy-code target=210086 tunnel=test-tunnel-id" || { echo " audit line missing brand tunnel id"; echo "$log"; return 1; }
|
|
444
|
+
return 0
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# CASE E: only a foreign (co-resident) connector present → untouched, brand still spawns.
|
|
448
|
+
case_reap_foreign_untouched() {
|
|
449
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
450
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
451
|
+
install_reap_stubs "$root"
|
|
452
|
+
local home="$root/home"
|
|
453
|
+
write_branded_config "$home/.maxy-code/cloudflared/config.yml"
|
|
454
|
+
cat > "$root/ps_table" <<TBL
|
|
455
|
+
777 cloudflared --origincert /home/other/.realagent/cloudflared/cert.pem --config /home/other/.realagent/cloudflared/config.yml tunnel run
|
|
456
|
+
TBL
|
|
457
|
+
|
|
458
|
+
run_with_reap "$root" ".maxy-code"
|
|
459
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
460
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
461
|
+
cleanup "$root"
|
|
462
|
+
|
|
463
|
+
echo "$log" | grep -q "op=reap brand=maxy-code found=0 killed=0 survivor=none" || { echo " census wrong"; echo "$log"; return 1; }
|
|
464
|
+
[ -z "$rec" ] || { echo " foreign connector signalled"; echo "$rec"; return 1; }
|
|
465
|
+
echo "$log" | grep -q "spawned service=cloudflared-maxy-code.service" || { echo " did not spawn own connector"; echo "$log"; return 1; }
|
|
466
|
+
return 0
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# CASE G: service active but MainPID unresolved → must NOT reap (can't tell the
|
|
470
|
+
# live supervised connector from a stray), must skip without spawning.
|
|
471
|
+
case_reap_skip_when_mainpid_unresolved() {
|
|
472
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
473
|
+
setup "$root" ".maxy-code" "active" "yes"
|
|
474
|
+
install_reap_stubs "$root"
|
|
475
|
+
local home="$root/home"
|
|
476
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
477
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
478
|
+
write_branded_config "$cfg"
|
|
479
|
+
# Override systemctl so MainPID resolves empty while is-active stays active
|
|
480
|
+
# (the RestartSec transition window).
|
|
481
|
+
cat > "$root/bin/systemctl" <<'SH'
|
|
482
|
+
#!/bin/bash
|
|
483
|
+
if [[ "$*" == *"is-active"* ]]; then echo "active"; exit 0; fi
|
|
484
|
+
if [[ "$*" == *"--property=MainPID"* ]]; then echo ""; fi
|
|
485
|
+
exit 0
|
|
486
|
+
SH
|
|
487
|
+
chmod +x "$root/bin/systemctl"
|
|
488
|
+
cat > "$root/ps_table" <<TBL
|
|
489
|
+
210086 cloudflared --no-autoupdate --origincert $cert --config $cfg tunnel run
|
|
490
|
+
TBL
|
|
491
|
+
|
|
492
|
+
run_with_reap "$root" ".maxy-code"
|
|
493
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
494
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
495
|
+
cleanup "$root"
|
|
496
|
+
|
|
497
|
+
[ -z "$rec" ] || { echo " reaped while survivor unknown — would kill the live connector"; echo "$rec"; return 1; }
|
|
498
|
+
echo "$log" | grep -q "skipped reason=mainpid-unresolved" || { echo " missing mainpid-unresolved skip log"; echo "$log"; return 1; }
|
|
499
|
+
echo "$log" | grep -q "spawned service=" && { echo " spawned while active"; echo "$log"; return 1; }
|
|
500
|
+
return 0
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# CASE F: stray survives SIGTERM → escalates to SIGKILL.
|
|
504
|
+
case_reap_sigkill_escalation() {
|
|
505
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
506
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
507
|
+
install_reap_stubs "$root"
|
|
508
|
+
local home="$root/home"
|
|
509
|
+
local cert="$home/.maxy-code/cloudflared/cert.pem"
|
|
510
|
+
local cfg="$home/.maxy-code/cloudflared/config.yml"
|
|
511
|
+
write_branded_config "$cfg"
|
|
512
|
+
touch "$root/reap_alive" # recorder reports the proc still alive on -0
|
|
513
|
+
cat > "$root/ps_table" <<TBL
|
|
514
|
+
210086 cloudflared --origincert $cert --config $cfg tunnel run
|
|
515
|
+
TBL
|
|
516
|
+
|
|
517
|
+
run_with_reap "$root" ".maxy-code" RESUME_TUNNEL_REAP_GRACE=0
|
|
518
|
+
local rec; rec=$(cat "$root/reap_log" 2>/dev/null || echo "")
|
|
519
|
+
cleanup "$root"
|
|
520
|
+
|
|
521
|
+
echo "$rec" | grep -q -- "-KILL 210086" || { echo " no SIGKILL escalation"; echo "$rec"; return 1; }
|
|
522
|
+
return 0
|
|
523
|
+
}
|
|
524
|
+
|
|
279
525
|
run_case "skip when service active" case_skip_when_scope_active
|
|
280
526
|
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
281
527
|
run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
|
|
282
528
|
run_case "service unit name derived from configDir" case_scope_unit_name_from_config_dir
|
|
283
529
|
run_case "slice flag passed to systemd-run" case_slice_flag_present
|
|
284
530
|
run_case "supervision contract: restart-always + no-autoupdate + .service" case_supervision_contract
|
|
531
|
+
run_case "reap: stray reaped then spawn (inactive)" case_reap_stray_then_spawn
|
|
532
|
+
run_case "reap: keeps supervised survivor, no spawn (active)" case_reap_keeps_survivor
|
|
533
|
+
run_case "reap: idempotent when single+active" case_reap_idempotent
|
|
534
|
+
run_case "reap: audit line names brand tunnel id" case_reap_audit_tunnel_id
|
|
535
|
+
run_case "reap: foreign co-resident connector untouched" case_reap_foreign_untouched
|
|
536
|
+
run_case "reap: SIGTERM survivor escalates to SIGKILL" case_reap_sigkill_escalation
|
|
537
|
+
run_case "reap: skip when active unit's MainPID is unresolved" case_reap_skip_when_mainpid_unresolved
|
|
285
538
|
|
|
286
539
|
echo ""
|
|
287
540
|
echo "Results: $PASS passed, $FAIL failed"
|
|
@@ -21,7 +21,11 @@ import { dirname, join, relative, sep, basename } from 'node:path'
|
|
|
21
21
|
import { fileURLToPath } from 'node:url'
|
|
22
22
|
|
|
23
23
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
24
|
-
|
|
24
|
+
// REPO_ROOT is overridable so the `.test.sh` self-test can point the gate at a
|
|
25
|
+
// fixture tree without dropping citation-bearing files into the real corpus.
|
|
26
|
+
const REPO_ROOT = process.env.TASK_ID_LEAK_ROOT
|
|
27
|
+
? process.env.TASK_ID_LEAK_ROOT
|
|
28
|
+
: join(SCRIPT_DIR, '..', '..')
|
|
25
29
|
|
|
26
30
|
const SCAN_ROOTS = [
|
|
27
31
|
join(REPO_ROOT, 'platform', 'scripts'),
|
|
@@ -37,6 +41,26 @@ const SKIP_DIR_NAMES = new Set([
|
|
|
37
41
|
])
|
|
38
42
|
const TASK_ID_RE = /\(Task \d+\)/
|
|
39
43
|
|
|
44
|
+
// Markdown mode. Unlike code files — where a citation is legal in a comment and
|
|
45
|
+
// only the parenthetical operator-visible form is rejected — a shipped `.md`
|
|
46
|
+
// loads into a client-facing agent's context wholesale, so ANY citation form
|
|
47
|
+
// leaks. Catches `Task NNN`, `(Task NNN)`, `Tasks NNN`, `since Task NNN`. It
|
|
48
|
+
// deliberately does NOT match bare `#NNN`: every `#NNN` in the corpus is a hex
|
|
49
|
+
// colour and there is no signal to separate `#732`-issue from `#732`-hex.
|
|
50
|
+
// Two alternatives:
|
|
51
|
+
// `\bTasks?[\s-]+\d{2,4}` — capital-T, any separator: `Task 732`, `(Task 732)`,
|
|
52
|
+
// `Tasks 12`, the hyphenated `Pre-Task-748` / `post-Task-904` forms.
|
|
53
|
+
// `\btasks?\s+\d{2,4}` — lowercase but SPACE-separated only: catches prose
|
|
54
|
+
// citations like "before task 127", while a lowercase HYPHENATED `task-537`
|
|
55
|
+
// stays exempt because that shape is a branch slug or a `.tasks/<n>-…-task-NNN.md`
|
|
56
|
+
// file-path reference (file paths are out of this gate's scope). The 2-digit
|
|
57
|
+
// floor keeps single-digit domain counts ("task 1 of 3") out.
|
|
58
|
+
const MD_TASK_ID_RE = /\bTasks?[\s-]+\d{2,4}|\btasks?\s+\d{2,4}/
|
|
59
|
+
const MD_SCAN_ROOTS = [
|
|
60
|
+
join(REPO_ROOT, 'platform', 'plugins'),
|
|
61
|
+
join(REPO_ROOT, 'platform', 'templates'),
|
|
62
|
+
]
|
|
63
|
+
|
|
40
64
|
function isCommentLine(line) {
|
|
41
65
|
const trimmed = line.replace(/^\s+/, '')
|
|
42
66
|
if (trimmed.startsWith('#')) return true
|
|
@@ -97,14 +121,38 @@ for (const root of SCAN_ROOTS) {
|
|
|
97
121
|
}
|
|
98
122
|
}
|
|
99
123
|
|
|
124
|
+
// Markdown scan — shipped operator-loadable docs. Reuses `walk` (which already
|
|
125
|
+
// skips node_modules/dist/build/.next/payload/__tests__) and matches every line
|
|
126
|
+
// against MD_TASK_ID_RE with no comment skip: the whole doc is agent content.
|
|
127
|
+
// .docs/ and .tasks/ are not under MD_SCAN_ROOTS, so they stay citation-rich.
|
|
128
|
+
for (const root of MD_SCAN_ROOTS) {
|
|
129
|
+
for (const file of walk(root)) {
|
|
130
|
+
if (!file.endsWith('.md')) continue
|
|
131
|
+
let content
|
|
132
|
+
try {
|
|
133
|
+
content = readFileSync(file, 'utf-8')
|
|
134
|
+
} catch {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
const lines = content.split('\n')
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const m = lines[i].match(MD_TASK_ID_RE)
|
|
140
|
+
if (!m) continue
|
|
141
|
+
leaks.push({ file: relative(REPO_ROOT, file), line: i + 1, content: m[0] })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
100
146
|
if (leaks.length > 0) {
|
|
101
|
-
console.error(`check-no-task-id-leaks: ${leaks.length}
|
|
147
|
+
console.error(`check-no-task-id-leaks: ${leaks.length} task-id citation(s) found:`)
|
|
102
148
|
for (const leak of leaks) {
|
|
103
|
-
console.error(`
|
|
149
|
+
console.error(`[task-id-leak] file=${leak.file} line=${leak.line} match=${leak.content}`)
|
|
104
150
|
}
|
|
105
151
|
console.error('')
|
|
106
|
-
console.error('
|
|
107
|
-
console.error('(stderr/stdout,
|
|
108
|
-
console.error('
|
|
152
|
+
console.error('Code files: citations belong in inline comments only, never in operator-visible')
|
|
153
|
+
console.error('strings (stderr/stdout, logs, systemd --description=, error messages, console.*).')
|
|
154
|
+
console.error('Shipped markdown (platform/plugins, platform/templates): no task citation in any')
|
|
155
|
+
console.error('form — the whole doc loads into a client-facing agent. Dev docs (.docs/, .tasks/)')
|
|
156
|
+
console.error('are exempt and keep their citations.')
|
|
109
157
|
process.exit(1)
|
|
110
158
|
}
|