@inetafrica/open-claudia 3.1.24 → 3.1.26
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/CHANGELOG.md +12 -0
- package/bin/cli.js +2 -2
- package/bin/keyring.js +19 -0
- package/bin/schedule.js +4 -3
- package/core/actions.js +140 -15
- package/core/context.js +22 -2
- package/core/dream.js +23 -3
- package/core/enforcer.js +184 -72
- package/core/entities.js +5 -1
- package/core/handlers.js +16 -6
- package/core/mandates.js +160 -0
- package/core/pack-review.js +8 -6
- package/core/providers/claude-hook.js +10 -10
- package/core/providers/claude.js +1 -0
- package/core/providers/codex-hook.js +5 -1
- package/core/relationship.js +4 -1
- package/core/router.js +51 -13
- package/core/scheduler.js +62 -2
- package/core/tool-guard.js +100 -1
- package/docs/permissions-audit-2026-07.md +324 -0
- package/package.json +4 -2
- package/test-approval-cards.js +184 -0
- package/test-enforcer.js +137 -0
- package/test-external-rule-scopes.js +8 -0
- package/test-fixtures/fake-agent-cli.js +1 -1
- package/test-mandate-hygiene.js +147 -0
- package/test-persona-pipeline.js +22 -8
- package/test-provider-dream.js +14 -2
- package/test-provider-enforcer.js +5 -1
- package/test-provider-gateway.js +16 -0
- package/test-provider-language.js +21 -1
- package/test-provider-pack-review.js +12 -1
- package/test-provider-scheduler.js +55 -22
- package/test-provider-tool-hooks.js +4 -0
- package/test-tool-guard.js +118 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Permissions system — production-readiness audit & redesign plan
|
|
2
|
+
|
|
3
|
+
Date: 2026-07-24 · Auditor: Open Claudia (session-reviewed, all fixes test-pinned)
|
|
4
|
+
Scope: the external-speaker guardrail stack — relationship classification, the
|
|
5
|
+
three policy layers, the enforcer (reply/action/triage vets), approvals +
|
|
6
|
+
owner cards, the tool deny-gate, mandates, and identity/people records.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Verdict
|
|
11
|
+
|
|
12
|
+
**Conditionally production-ready.** The core is sound: default-deny
|
|
13
|
+
classification, an independent model judge that fails closed, owner-only
|
|
14
|
+
approval, and a hardcoded security floor no note can override. This pass
|
|
15
|
+
closed the five gaps that were outright exploitable or operationally broken
|
|
16
|
+
(F1–F6 below). What remains open is not exploitable-by-message but is
|
|
17
|
+
*organizational* debt that will bite as more people use the bot: fragmented
|
|
18
|
+
identities, no group layer, and always-allow grants that are broader than the
|
|
19
|
+
owner probably intends. Those are designed (§6) but deliberately NOT built
|
|
20
|
+
until the plan is approved.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. The system today (post-fix architecture)
|
|
25
|
+
|
|
26
|
+
Layers consulted for every guarded turn, narrowing in scope; union of grants,
|
|
27
|
+
any prohibition wins, default-deny:
|
|
28
|
+
|
|
29
|
+
1. **Security floor** — hardcoded in `GUARD_SYSTEM_PROMPT` (enforcer.js).
|
|
30
|
+
Credentials/secrets/infra access can never be granted by any layer below.
|
|
31
|
+
2. **Global guardrails** — owner-authored, applies to every external.
|
|
32
|
+
3. **Channel rules** — per group chat / task thread.
|
|
33
|
+
4. **Person mandate** — per-person grants on their entity pack (≤4000 chars,
|
|
34
|
+
deduped, slug-keyed; judge reads at most 6000 defensively).
|
|
35
|
+
5. **Turn grant** — one-off "handle this request" approval; reply vets only.
|
|
36
|
+
|
|
37
|
+
Enforcement points on a guarded (non-owner) turn:
|
|
38
|
+
|
|
39
|
+
- **Inbound triage** (router → `guardInboundTriage`): the request itself is
|
|
40
|
+
judged BEFORE any agent work. Held → owner card ("should I handle it?"),
|
|
41
|
+
external gets the holding line, no generation happens at all.
|
|
42
|
+
- **Provider file tools** (PreToolUse hook → tool-guard): Write/Edit/
|
|
43
|
+
MultiEdit/NotebookEdit are denied outright for external speakers, with a
|
|
44
|
+
redirect to write-tier tools (which escalate to the owner).
|
|
45
|
+
- **Bash deny-gate** (unchanged): prod surfaces require the covering
|
|
46
|
+
open-claudia tool; write/destructive tool runs escalate with exact-command
|
|
47
|
+
approval, one-shot consume, byte-bound.
|
|
48
|
+
- **Outbound reply vet** (`guardOutboundReply`): the authoritative gate on
|
|
49
|
+
anything the external actually receives. Triage is advisory; this is final.
|
|
50
|
+
|
|
51
|
+
Identity: `speakerFor` fails closed to "external"; owner status comes only
|
|
52
|
+
from `people.isOwner` via canonical identity (a note can never confer it).
|
|
53
|
+
Unknown authed speakers get an auto-contact (fresh record, hashed entity slug,
|
|
54
|
+
never name-linked). Meet-greet lane covers UNVERIFIED first contacts.
|
|
55
|
+
|
|
56
|
+
Fail direction map (deliberate asymmetries):
|
|
57
|
+
|
|
58
|
+
| Gate | On internal error | Why |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| speakerFor | closed (external) | never widen on failure |
|
|
61
|
+
| Enforcer verdict | closed (escalate), never cached, 1 bounded retry on transient | transport blips ≠ verdicts |
|
|
62
|
+
| Triage (guarded speaker) | closed (held) | reply vet still downstream |
|
|
63
|
+
| Triage (unresolvable speaker) | open (proceed) | owner UX; output vet authoritative |
|
|
64
|
+
| File-mutation gate, no channel env | dormant (allow) | owner-local turns untouched (R9/R14) |
|
|
65
|
+
| File-mutation gate, env + resolve error | closed (deny) | env present ⇒ real channel turn |
|
|
66
|
+
| Bash deny-gate | open (allow) | availability guard, not identity guard; logged |
|
|
67
|
+
| Turn grant validation | closed (no grant) | any mismatch/expiry/read error |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Findings fixed in this pass
|
|
72
|
+
|
|
73
|
+
| ID | Sev | Finding | Fix (all uncommitted, test-pinned) |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| F1 | High (ops) | Guard model cold-starts overran the old timeout, failing closed on legitimate replies → spurious owner cards | 60s timeout (`ENFORCER_TIMEOUT_MS`) + exactly one retry on transient transport errors; non-transient never retried; persistent failure still closes |
|
|
76
|
+
| F2 | **Critical** | Only Bash was hooked: an external speaker could have the agent write/edit files directly via provider Write/Edit tools — bypassing the whole approval path | Second PreToolUse matcher `^(Write\|Edit\|MultiEdit\|NotebookEdit)$` → same deny-gate CLI → speaker-gated denial; deliberately outside relaxed tooling-mode (it's a security control, not a workflow preference); audited as `external-file-write` |
|
|
77
|
+
| F3 | Med (UX/authz clarity) | Approval cards led with the bot's output, not the person — the owner was approving text, unclear they were authorizing a PERSON's request | Cards reworded: "**X** asked me something — should I act on it?" / "wants me to handle a request — should I?" / "wants me to run a **write** action — should I?"; ask shown first |
|
|
78
|
+
| F4 | High | Work happened BEFORE any approval: the agent fully processed an external request, only the reply was vetted (cost, prompt-injection surface, side-effect risk) | Up-front triage gate in the router: inbound judged first; held turns generate nothing; approval re-runs enter via the scheduler so they can't re-triage into a loop |
|
|
79
|
+
| F5 | High (friction→risk) | Without a grant concept, every triage-approved request structurally produced a SECOND approval card for its reply — double-approval fatigue trains blanket-allow habits | Turn grant: the approval id rides the scheduler job → bot-process AsyncLocalStorage (agent subprocess can never forge it) → honored in reply vets ONLY, validated fail-closed (approved + triage-kind + adapter/channel match + <24h), quotes one request, never actions |
|
|
80
|
+
| F6 | High (latent bug) | `appendMandate` upserted by NAME: an always-allow for auto-contact `sharon-a1b2c3` could land on a different `sharon` entity — cross-person grant leakage; mandates also grew unbounded with duplicate lines | Slug-keyed upsert; whitespace-normalized dedup; 4000-char write cap (refused, never truncated — it's security text); 6000-char defensive read clip in the judge; same hygiene in the /guardrails person-rule path |
|
|
81
|
+
|
|
82
|
+
## 4. Known gaps — documented, not yet closed
|
|
83
|
+
|
|
84
|
+
| ID | Sev | Gap | Position |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| G1 | Med | Codex `apply_patch` file writes: hook matcher for Codex remains Bash-only; whether Codex file-mutation surfaces are covered is UNVERIFIED | Left unchanged rather than guessing hook names; verify against a live Codex fleet bot before relying on F2 parity there |
|
|
87
|
+
| G2 | Med | Media turns (voice/photo/document) from externals bypass inbound triage (text-only hook) | Output vet still covers everything they receive; close alongside G1 in a follow-up |
|
|
88
|
+
| G3 | ~~Low~~ CLOSED (P3) | ~~The `apr:` button handler (actions.js) has no test harness~~ Closed with P3: test-approval-cards.js pins the handler end-to-end (real approvals + entities + mandates, stubbed transport) — reply Always-allow delivery + scoped grant + ack ids, double-tap idempotence, deny hold, owner-only gating, triage approve wake job + deny decline, make-a-rule scope buttons, expired-card ack | Was: pre-existing posture; grant validation, card content, and record shapes were already pinned in test-enforcer.js |
|
|
89
|
+
| G4 | High (org) | Identity fragmentation: auto-contacts key on (adapter, userId) — the same human on Telegram + Kazee + Spaces mints separate records with separate mandates. An owner grant lands on one-third of a person (mechanism verified in code; live counts to be confirmed on the deployed store during the repair pass) | Redesign P1 |
|
|
90
|
+
| G5 | Med | No group layer: nothing between "this channel" and "this person" — can't say "the dev team may deploy to staging" | Redesign P2 |
|
|
91
|
+
| G6 | High (authz creep) | "Always allow" writes a verbatim-ask mandate line, forever, with no scope or expiry — over time a person accretes broad standing permissions the owner never reviewed as a set | Cap+dedup (F6) bound the bleeding; real fix is scoped grants, Redesign P3 |
|
|
92
|
+
|
|
93
|
+
## 5. Test coverage after this pass
|
|
94
|
+
|
|
95
|
+
All green locally (`npm test`; one known loaded-runner flake class in
|
|
96
|
+
test-utility-provider-policy re-verified 3× clean):
|
|
97
|
+
|
|
98
|
+
- **test-enforcer.js** (+137 lines): triage kind end-to-end (owner no-op,
|
|
99
|
+
allow, block→card with handle/decline buttons + pinned record, fail-closed),
|
|
100
|
+
turn grant (reply-only, quotes the ask, cache-key isolation, and rejection
|
|
101
|
+
of pending / wrong-channel / wrong-kind ids), retry semantics, fail-closed
|
|
102
|
+
+ never-cache, audit hygiene (no payloads/mandates in logs).
|
|
103
|
+
- **test-tool-guard.js** (+60): file gate dormant without channel env, denies
|
|
104
|
+
unknown-external Write/Edit/NotebookEdit, reads+benign Bash stay free,
|
|
105
|
+
owner turns untouched, audited rule, settings carry both matchers on one
|
|
106
|
+
hook command.
|
|
107
|
+
- **test-mandate-hygiene.js** (new): slug-keying (two Sharons never share),
|
|
108
|
+
dedup, cap-refusal leaves bytes identical, name-fallback for legacy records.
|
|
109
|
+
- **test-provider-gateway.js**: router consults triage with the routed text +
|
|
110
|
+
provider/project/dir pinning on every text turn.
|
|
111
|
+
- Plus the previously-passing enforcer/scheduler/hook/approval suites.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 6. Redesign plan (approval requested — nothing below is built)
|
|
116
|
+
|
|
117
|
+
### P1 — One person, many surfaces (identity unification)
|
|
118
|
+
|
|
119
|
+
Anchor: **people.id stays the canonical key; Kazee userId becomes the
|
|
120
|
+
preferred anchor handle** (it is unique across all Kazee tasks, so every task
|
|
121
|
+
thread resolves to the same person — no per-task duplicates).
|
|
122
|
+
|
|
123
|
+
- **Agent-proposed, owner-confirmed merges.** When a new identity appears (or
|
|
124
|
+
at repair time), the agent gathers signals — same display name, same email
|
|
125
|
+
in profile, cross-references in conversation ("this is Sharon from
|
|
126
|
+
accounts") — and posts a card: *"Is `telegram: Sharon N. (…111)` the same
|
|
127
|
+
person as `kazee: sharon@… `? [Same person] [Different] [Not sure]"*.
|
|
128
|
+
NEVER auto-merge, and never merge on display name alone (names are
|
|
129
|
+
spoofable — that invariant already exists in ensureContact and stays).
|
|
130
|
+
- **Merge = handles fold into one people record**; entities merge Mandate
|
|
131
|
+
(union, deduped, capped — over-cap requires owner trim), Notes/Log append.
|
|
132
|
+
The losing record is tombstoned (`mergedInto: <id>`) so old approval
|
|
133
|
+
records/audit lines still resolve. Merges are reversible from the tombstone.
|
|
134
|
+
- **Repair pass** for existing stores: one-time sweep proposes merge cards for
|
|
135
|
+
probable duplicates (e.g. the fragmented Sharon records on the deployed
|
|
136
|
+
bot); owner confirms each. Nothing merges silently.
|
|
137
|
+
- Guard changes: `speakerFor` unchanged (already person-keyed); the win is
|
|
138
|
+
that one mandate follows the human across surfaces.
|
|
139
|
+
|
|
140
|
+
### P2 — Groups as a policy layer
|
|
141
|
+
|
|
142
|
+
Precedence becomes: **floor > global > channel > group > person > turn-grant**
|
|
143
|
+
(same rule: grants union, any prohibition wins, floor absolute).
|
|
144
|
+
|
|
145
|
+
- A group is an owner-curated named set of person ids (`groups.json`), e.g.
|
|
146
|
+
`dev-team`, `family`. People can be in several.
|
|
147
|
+
- `/guardrails` gains group scope; the "Make a rule…" card offers *this
|
|
148
|
+
person / this channel / this group / everyone*.
|
|
149
|
+
- The enforcer prompt gains one GROUP RULES section (union of the speaker's
|
|
150
|
+
groups, clipped like mandates). Cache key includes it.
|
|
151
|
+
- No nesting, no inheritance between groups in v1 — flat sets only.
|
|
152
|
+
|
|
153
|
+
### P3 — Capability-scoped approvals (kills the blanket)
|
|
154
|
+
|
|
155
|
+
Replaces free-text "Always-allowed by owner: <verbatim ask>" with a
|
|
156
|
+
structured grant the owner explicitly shapes at approval time:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
- grant: {capability} · scope: {this-exact-thing | this-kind-of-thing}
|
|
160
|
+
· for: {person | group} · expires: {7d | 30d | never} · granted: 2026-07-24
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- The card's "Make a rule…" flow asks two more taps: *what* ("just this" vs
|
|
164
|
+
"requests like this — <agent-drafted capability line the owner can edit>")
|
|
165
|
+
and *how long* (7d / 30d / no expiry). Default is the NARROW option.
|
|
166
|
+
- Stored in the same Mandate section (human-readable lines, same cap), so the
|
|
167
|
+
judge consumes them without a format break; expired lines are skipped at
|
|
168
|
+
read time and swept by the nightly dream pass with a chat notice.
|
|
169
|
+
- `/guardrails` lists a person's active grants with per-line revoke — the
|
|
170
|
+
owner finally sees the accumulated permission set in one place.
|
|
171
|
+
- Rationale: an approval should carry WHO + WHAT KIND + HOW LONG, not become
|
|
172
|
+
an eternal verbatim precedent (this is the "blanket approve" concern raised
|
|
173
|
+
on the always-allow flow, solved at the model layer the judge already reads).
|
|
174
|
+
|
|
175
|
+
### P4 — Rollout order
|
|
176
|
+
|
|
177
|
+
1. **P3** first (smallest blast radius, immediate authz-creep fix; pure
|
|
178
|
+
card-flow + mandate-format change, judge prompt already compatible).
|
|
179
|
+
2. **P1** second (unification + repair pass; needs the merge card UI and the
|
|
180
|
+
tombstone plumbing; highest data-migration care — every step
|
|
181
|
+
owner-confirmed, reversible, backed up like dream merges).
|
|
182
|
+
3. **P2** last (new layer; cleanest once identities are unified so group
|
|
183
|
+
membership means one person, not one fragment).
|
|
184
|
+
4. Close G1/G2 (Codex file-tool verification, media-turn triage) alongside.
|
|
185
|
+
(G3, the `apr:` handler harness, shipped with P3 — see §4/§9.)
|
|
186
|
+
|
|
187
|
+
Risks: P1 merge cards interrupt the owner (batched, max a few per day);
|
|
188
|
+
P3 makes approvals two taps longer (only on "Make a rule", never on
|
|
189
|
+
approve-once); all three change nothing for the owner's own turns.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 7. Pass 2 — 360° dangling-pathway sweep (2026-07-24, post-go)
|
|
194
|
+
|
|
195
|
+
After the go, one full pass enumerating every EXISTING entry point that
|
|
196
|
+
reaches the same concerns as the new gates (the /intros lesson: a new system
|
|
197
|
+
must reconcile every old pathway, not just the one it was built on). Findings,
|
|
198
|
+
all fixed and test-pinned this pass:
|
|
199
|
+
|
|
200
|
+
- **D1 · CRITICAL — model output could write guard policy.** The per-turn
|
|
201
|
+
pack reviewer and the nightly dream both passed `relationship` + `mandate`
|
|
202
|
+
from model JSON into `upsertEntity`. Both run over mixed-provenance content
|
|
203
|
+
(including external chatter), so a prompt-injected turn could have granted
|
|
204
|
+
itself a mandate. Writers now strip both fields (owner actions remain the
|
|
205
|
+
only writers), prompts/schemas updated, attack fixtures added
|
|
206
|
+
(test-provider-pack-review, test-provider-dream). Introspection's
|
|
207
|
+
entity_edits applier verified notes-only.
|
|
208
|
+
- **D2 — media turns bypassed inbound triage (closes G2).** Voice, audio,
|
|
209
|
+
photo, and document handlers ran the agent without `guardInboundTriage`;
|
|
210
|
+
only text was triaged. All five inbound shapes now enter through one
|
|
211
|
+
`admitGuardedTurn` helper; media asks carry saved file paths so an
|
|
212
|
+
owner-approved re-run can still find them, and the resolved ask is
|
|
213
|
+
back-filled into the turn scope (`setCurrentInboundText`) so owner cards
|
|
214
|
+
and F5 turn-grant validation see the real request, not an empty string.
|
|
215
|
+
- **D3 — dream auto-merges could delete guard policy.** An entity merge
|
|
216
|
+
could remove a mandate-carrying entity or orphan a people-record
|
|
217
|
+
`entitySlug` pointer (silent speaker-resolution break). Merges touching
|
|
218
|
+
either are now skipped (`guardPinned`) — owner-confirmed merges only
|
|
219
|
+
(arrives with P1).
|
|
220
|
+
- **D4 — Bash was an open side door around F2/F6.** On a guarded turn one
|
|
221
|
+
heredoc could edit config, packs, crons, tool sources, or the installed
|
|
222
|
+
package. New speaker-gated control-plane gate in the PreToolUse hook:
|
|
223
|
+
any command referencing the config dir, package root, or `.open-claudia`
|
|
224
|
+
is denied for externals (reads too — read-then-quote leaks policy), ahead
|
|
225
|
+
of the tooling-mode switch, audited as `external-control-plane`.
|
|
226
|
+
Authorised channels stay exempt (bin/tool.js gates externals itself).
|
|
227
|
+
- **D5 — Codex file edits were unhooked (closes G1, live-probed).** Probed
|
|
228
|
+
real codex 0.144.0 with a log-everything hook: shell arrives as tool_name
|
|
229
|
+
`Bash`, file edits as `apply_patch` — and normal runs bypass the sandbox,
|
|
230
|
+
so the `^Bash$` matcher left externals able to write ANY file under Codex.
|
|
231
|
+
Matcher widened to `^(Bash|apply_patch)$`, `apply_patch` added to
|
|
232
|
+
FILE_MUTATION_TOOLS, fixture + pins updated.
|
|
233
|
+
- **D6 — two mandate writers, drift risk.** The always-allow grant writer
|
|
234
|
+
(actions.appendMandate) and the rule-card writer (router) each implemented
|
|
235
|
+
dedup/cap/slug-keying. Unified on one `appendMandateLine` so hygiene can't
|
|
236
|
+
drift.
|
|
237
|
+
- **D7 — keyring CLI was speaker-blind.** `keyring exec` runs an arbitrary
|
|
238
|
+
command with the FULL keyring in env, `get` prints raw values — and the
|
|
239
|
+
control-plane gate exempts `keyring exec` as an authorised channel, so on
|
|
240
|
+
an external turn it was a creds-plus-config-write side door around D4. The
|
|
241
|
+
keyring CLI now refuses guarded external speakers outright (every verb,
|
|
242
|
+
audited as `external-keyring`), dormant owner-local, denied on resolution
|
|
243
|
+
failure.
|
|
244
|
+
|
|
245
|
+
Verified-OK pathways (no change needed): scheduler re-runs stamp
|
|
246
|
+
`OC_CHANNEL_USER_ID` (file gate stays live on approved external re-runs);
|
|
247
|
+
Spaces turns flow through the common router (triage covered); sub-agents
|
|
248
|
+
spawn read-only and inherit channel env; codex transport confinement
|
|
249
|
+
(hooks on, plugins/unified_exec off, untrusted trust) is mandatory-or-refuse.
|
|
250
|
+
|
|
251
|
+
G3 closed with P3 (test-approval-cards.js pins the `apr:` handler). `tool run`
|
|
252
|
+
remains the one authorised channel reachable on external turns — by design:
|
|
253
|
+
bin/tool.js vets write/destructive tiers per speaker (mandate-allow or
|
|
254
|
+
owner escalation; vet logic pinned in test-enforcer.js /
|
|
255
|
+
test-provider-enforcer.js), and read-only verbs answer without side effects.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 8. Ship state
|
|
260
|
+
|
|
261
|
+
Go received 2026-07-24. All §3/§5 fixes plus the §7 sweep are committed and
|
|
262
|
+
pushed (v3.1.25, 3025f53); fleet roll/publish is confirmed separately
|
|
263
|
+
(restarting the bot kills the live session). §6 starts next: P3 → P1 → P2.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 9. P3 — capability-scoped approvals (2026-07-24)
|
|
268
|
+
|
|
269
|
+
The owner's driving concern, verbatim: "the approval all is a blanket for all
|
|
270
|
+
users or how can we get it to be for the user plus maybe what we want them to
|
|
271
|
+
be able to do as a guardrail vs a clanket approve everything going forward for
|
|
272
|
+
all or event that person." A mandate line is now a scoped, expiring, revocable
|
|
273
|
+
GRANT — not a permanent blanket.
|
|
274
|
+
|
|
275
|
+
New module `core/mandates.js`; the three policy layers now share one
|
|
276
|
+
lifecycle (id → list → revoke):
|
|
277
|
+
|
|
278
|
+
- **Ids.** Every mandate bullet has a stable 8-char id =
|
|
279
|
+
sha1(slug + normalized text) — slug-scoped, so the same capability granted
|
|
280
|
+
to two people revokes independently. Ids ignore case, whitespace, and the
|
|
281
|
+
expiry marker.
|
|
282
|
+
- **Expiry.** A trailing ` [until YYYY-MM-DD]` marker (UTC, inclusive).
|
|
283
|
+
Tap-grants ("Always allow" on an approval card) default to a bounded TTL —
|
|
284
|
+
`MANDATE_GRANT_TTL_DAYS`, 30 by default, 0 = permanent. Owner-authored rule
|
|
285
|
+
cards stay permanent unless the text ends "for Nd" / "until YYYY-MM-DD"
|
|
286
|
+
(`extractExpiry`). Filtering is deterministic and read-side:
|
|
287
|
+
`relationship.speakerFor` (the single choke point every consumer of
|
|
288
|
+
`speaker.mandate` goes through — enforcer, external-mode prompt, tool vets)
|
|
289
|
+
drops expired bullets via `activeMandateText`, so the judge never sees a
|
|
290
|
+
lapsed capability and never does date math. The enforcer verdict cache keys
|
|
291
|
+
on the filtered text, so expiry busts it naturally.
|
|
292
|
+
- **Refresh, not stacking.** `appendMandateLine` dedups on the
|
|
293
|
+
marker-stripped text: re-granting the same capability replaces the line
|
|
294
|
+
with the new expiry (`refreshed: true`). Successful writes also prune
|
|
295
|
+
expired bullets and collapse legacy duplicates; refused/duplicate writes
|
|
296
|
+
mutate nothing (cap-refusal pin unchanged). Non-bullet hand-written text
|
|
297
|
+
survives rewrites.
|
|
298
|
+
- **Revoke.** `/guardrails` now lists per-person grants (`👤 name (slug)`,
|
|
299
|
+
ids + until-dates) alongside global + channel rules; `remove|rm|revoke <id>`
|
|
300
|
+
resolves across all three layers (global → channel → person). Revoking the
|
|
301
|
+
last line empties the section via direct `writeEntity` (upsertEntity treats
|
|
302
|
+
"" as no-op). Approval-card acks quote the id + expiry and the revoke
|
|
303
|
+
command.
|
|
304
|
+
|
|
305
|
+
Pathways reconciled (the §7 discipline applied to the new system):
|
|
306
|
+
`bin/entity.js persona --mandate` is a deliberate owner-only WHOLESALE editor
|
|
307
|
+
(same class as hand-editing the entity file) — the parser tolerates whatever
|
|
308
|
+
lands there and the read filter still governs what the judge sees. Dream and
|
|
309
|
+
pack-review remain stripped of mandate writes (D1). All other writers funnel
|
|
310
|
+
through `appendMandateLine` (D6).
|
|
311
|
+
|
|
312
|
+
Pinned in test-mandate-hygiene.js (expiry marker, refresh, prune-on-write,
|
|
313
|
+
id scoping, extractExpiry, list/revoke, last-line clear) and
|
|
314
|
+
test-external-rule-scopes.js (speakerFor drops expired lines while the file
|
|
315
|
+
still holds them). G3 closes here too: test-approval-cards.js exercises the
|
|
316
|
+
`apr:` button handler end-to-end with the real approvals + entities +
|
|
317
|
+
mandates modules and stubbed transport — Always-allow delivers AND writes the
|
|
318
|
+
scoped grant (TTL marker, ack quotes id + revoke command), double-tap can't
|
|
319
|
+
flip a decision or duplicate a grant, deny holds with no delivery and no
|
|
320
|
+
mandate write, non-owner taps bounce, triage approve wakes the agent in the
|
|
321
|
+
origin channel with the approval id as a one-off turn grant (request framed
|
|
322
|
+
strictly as data), triage deny sends the polite decline without waking the
|
|
323
|
+
agent, "Make a rule" delivers and offers person/channel/global scope buttons,
|
|
324
|
+
and stale cards get a clean ack.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.26",
|
|
4
4
|
"description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
13
13
|
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
|
|
14
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
|
|
14
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
|
@@ -69,7 +69,9 @@
|
|
|
69
69
|
"test-recall-relationship-gate.js",
|
|
70
70
|
"test-relationship.js",
|
|
71
71
|
"test-enforcer.js",
|
|
72
|
+
"test-mandate-hygiene.js",
|
|
72
73
|
"test-external-rule-scopes.js",
|
|
74
|
+
"test-approval-cards.js",
|
|
73
75
|
"test-identity-prune.js",
|
|
74
76
|
"test-kazee-message-id.js",
|
|
75
77
|
"test-kazee-channel-health.js",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
// G3: the apr: button handler finally has a harness. The approval card is the
|
|
6
|
+
// owner's authorization surface for guarded external turns, so its handler is
|
|
7
|
+
// pinned end-to-end with the real approvals + entities + mandates modules and
|
|
8
|
+
// stubbed transport: reply delivery, Always-allow grant (scoped + expiring,
|
|
9
|
+
// P3), triage wake jobs, deny paths, owner-only gating, and the make-a-rule
|
|
10
|
+
// scope prompt.
|
|
11
|
+
|
|
12
|
+
const assert = require("assert");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "approval-cards-"));
|
|
18
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
19
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = path.join(tmp, "config");
|
|
20
|
+
process.env.WORKSPACE = path.join(tmp, "workspace");
|
|
21
|
+
process.env.ENTITIES_DIR = path.join(tmp, "entities");
|
|
22
|
+
process.env.PEOPLE_FILE = path.join(tmp, "people.json");
|
|
23
|
+
process.env.APPROVALS_DIR = path.join(tmp, "approvals");
|
|
24
|
+
process.env.TELEGRAM_CHAT_ID = "owner-chat";
|
|
25
|
+
process.env.CHANNELS = "";
|
|
26
|
+
process.env.CLAUDE_PATH = process.execPath;
|
|
27
|
+
process.env.CODEX_PATH = process.execPath;
|
|
28
|
+
fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
|
|
29
|
+
fs.mkdirSync(process.env.WORKSPACE, { recursive: true });
|
|
30
|
+
|
|
31
|
+
function installStub(relativePath, exports) {
|
|
32
|
+
const filename = require.resolve(relativePath);
|
|
33
|
+
require.cache[filename] = { id: filename, filename, loaded: true, exports };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sends = [];
|
|
37
|
+
const relayed = [];
|
|
38
|
+
const jobsAdded = [];
|
|
39
|
+
|
|
40
|
+
installStub("./core/io", {
|
|
41
|
+
send: async (text, opts) => { sends.push({ text, opts }); return { ok: true, messageId: String(sends.length) }; },
|
|
42
|
+
deleteMessage: async () => true,
|
|
43
|
+
});
|
|
44
|
+
installStub("./core/state", {
|
|
45
|
+
currentState: () => ({ settings: {}, messageQueue: [] }),
|
|
46
|
+
saveState: () => {},
|
|
47
|
+
getActiveProvider: () => "claude",
|
|
48
|
+
getProjectKey: () => "fixture",
|
|
49
|
+
consumeSessionCallbackToken: () => null,
|
|
50
|
+
userOwnsProviderSession: () => true,
|
|
51
|
+
});
|
|
52
|
+
installStub("./core/runner", { admitRunContext: () => ({}), runAgent: async () => ({ ok: true }) });
|
|
53
|
+
installStub("./core/access", {
|
|
54
|
+
isChatOwner: (channelId) => String(channelId) === "owner-chat",
|
|
55
|
+
approveAuthRequest: () => false, denyAuthRequest: () => false, authRequestLabel: () => "",
|
|
56
|
+
listAuthorizedRaw: () => ({ authorized: [], pending: [] }),
|
|
57
|
+
});
|
|
58
|
+
installStub("./core/people", { findByHandle: () => null });
|
|
59
|
+
installStub("./core/vault-store", { vault: { exists: () => false, isUnlocked: () => false } });
|
|
60
|
+
installStub("./core/intros", {});
|
|
61
|
+
installStub("./core/intro-flow", {});
|
|
62
|
+
installStub("./core/onboarding", { finishOnboarding: async () => {} });
|
|
63
|
+
installStub("./core/handlers", {
|
|
64
|
+
startSession: async () => {}, switchProviderState: async () => {}, startNewConversation: async () => {},
|
|
65
|
+
endCurrentSession: async () => {}, activeContinueOptions: () => null, applyProviderSetting: async () => {},
|
|
66
|
+
performProviderLogout: async () => {}, startProviderLogin: async () => {},
|
|
67
|
+
});
|
|
68
|
+
installStub("./core/providers", {});
|
|
69
|
+
installStub("./core/jobs", {});
|
|
70
|
+
installStub("./core/scheduler", { addJob: (job) => { jobsAdded.push(job); return { id: `job-${jobsAdded.length}` }; } });
|
|
71
|
+
installStub("./core/side-chat", { sideChatCoordinator: { enqueue: async () => ({ status: "queued" }) } });
|
|
72
|
+
installStub("./core/auth-flow", {
|
|
73
|
+
claudeProviderAuthStatus: async () => ({}), runClaudeAuthCommand: async () => {},
|
|
74
|
+
runCodexLoginStatus: async () => ({}), runCodexDeviceLogin: async () => {},
|
|
75
|
+
canLogout: () => false, providerAccountSummary: async () => "",
|
|
76
|
+
});
|
|
77
|
+
installStub("./core/relay", {
|
|
78
|
+
send: async (msg) => { relayed.push(msg); return { ok: true }; },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const entities = require("./core/entities");
|
|
82
|
+
const mandates = require("./core/mandates");
|
|
83
|
+
const approvals = require("./core/approvals");
|
|
84
|
+
const { handleAction } = require("./core/actions");
|
|
85
|
+
|
|
86
|
+
const edits = [];
|
|
87
|
+
function envelopeFor(payload, { channelId = "owner-chat", sourceMessageId = "card-1" } = {}) {
|
|
88
|
+
return {
|
|
89
|
+
adapter: {
|
|
90
|
+
id: "kazee-fixture", type: "kazee",
|
|
91
|
+
edit: async (chan, srcId, msg) => { edits.push({ chan, srcId, msg }); },
|
|
92
|
+
},
|
|
93
|
+
channelId,
|
|
94
|
+
action: { payload, sourceMessageId },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const lastEdit = () => edits[edits.length - 1]?.msg || "";
|
|
98
|
+
const lastSend = () => sends[sends.length - 1]?.text || "";
|
|
99
|
+
|
|
100
|
+
(async () => {
|
|
101
|
+
entities.upsertEntity({ slug: "lucy-ext1", name: "Lucy", relationship: "external" });
|
|
102
|
+
const base = {
|
|
103
|
+
adapter: "kazee", channelId: "ext-channel", canonicalUserId: "owner-user", userId: "user-lucy",
|
|
104
|
+
provider: "claude", project: "fixture", personName: "Lucy", entitySlug: "lucy-ext1",
|
|
105
|
+
targetAdapter: "kazee", targetChannel: "ext-channel",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ── (b) reply escalation: Always-allow delivers AND writes a scoped grant ──
|
|
109
|
+
const reply = approvals.create({ ...base, kind: "reply", payloadText: "Your invoice is settled." });
|
|
110
|
+
await handleAction(envelopeFor(`apr:${reply.id}:always`));
|
|
111
|
+
assert.strictEqual(relayed.length, 1, "approved reply delivered to the external");
|
|
112
|
+
assert.strictEqual(relayed[0].text, "Your invoice is settled.");
|
|
113
|
+
assert.strictEqual(relayed[0].target.channelId, "ext-channel");
|
|
114
|
+
assert.strictEqual(approvals.read(reply.id).status, "approved");
|
|
115
|
+
const grantId = mandates.lineId("lucy-ext1", "Always-allowed by owner: reply about: Your invoice is settled.");
|
|
116
|
+
const mandateText = entities.readEntity("lucy-ext1").sections.Mandate;
|
|
117
|
+
assert.ok(/Always-allowed by owner: reply about: Your invoice is settled\./.test(mandateText), "grant written");
|
|
118
|
+
const exp = mandates.defaultGrantExpiry();
|
|
119
|
+
assert.ok(mandateText.includes(`[until ${exp}]`), "tap-grant carries the default TTL (scoped, not blanket)");
|
|
120
|
+
assert.ok(lastEdit().includes(`mandate [${grantId}]`), "ack quotes the grant id");
|
|
121
|
+
assert.ok(lastEdit().includes(`/guardrails revoke ${grantId}`), "ack offers the revoke command");
|
|
122
|
+
|
|
123
|
+
// Double-tap: decide() keeps the FIRST decision and the mandate is not
|
|
124
|
+
// re-written; delivery re-runs by design (retry affordance after a failed
|
|
125
|
+
// send — the card edit makes an accidental double-tap unlikely).
|
|
126
|
+
await handleAction(envelopeFor(`apr:${reply.id}:always`));
|
|
127
|
+
assert.strictEqual(approvals.read(reply.id).status, "approved", "second tap can't flip the decision");
|
|
128
|
+
assert.strictEqual(
|
|
129
|
+
entities.readEntity("lucy-ext1").sections.Mandate.split("invoice is settled").length, 2,
|
|
130
|
+
"double-tap never duplicates the grant");
|
|
131
|
+
|
|
132
|
+
// ── deny on a fresh reply: held, nothing delivered ──
|
|
133
|
+
relayed.length = 0;
|
|
134
|
+
const held = approvals.create({ ...base, kind: "reply", payloadText: "Payroll details attached." });
|
|
135
|
+
await handleAction(envelopeFor(`apr:${held.id}:deny`));
|
|
136
|
+
assert.strictEqual(approvals.read(held.id).status, "denied");
|
|
137
|
+
assert.strictEqual(relayed.length, 0, "denied reply never reaches the external");
|
|
138
|
+
assert.ok(/Held/.test(lastEdit()), "owner sees the hold ack");
|
|
139
|
+
assert.ok(!/Payroll/.test(entities.readEntity("lucy-ext1").sections.Mandate), "deny writes no mandate");
|
|
140
|
+
|
|
141
|
+
// ── owner-only: an external chat cannot decide approvals ──
|
|
142
|
+
const foreign = approvals.create({ ...base, kind: "reply", payloadText: "Secret." });
|
|
143
|
+
await handleAction(envelopeFor(`apr:${foreign.id}:ok`, { channelId: "intruder-chat" }));
|
|
144
|
+
assert.strictEqual(approvals.read(foreign.id).status, "pending", "non-owner tap changes nothing");
|
|
145
|
+
assert.ok(/Owner only/.test(lastSend()));
|
|
146
|
+
assert.strictEqual(relayed.length, 0);
|
|
147
|
+
|
|
148
|
+
// ── (c) triage approve: wakes the agent in the ORIGIN channel with the
|
|
149
|
+
// approval id as a one-off turn grant ──
|
|
150
|
+
const triage = approvals.create({ ...base, kind: "triage", inboundText: "Can you check invoice 42?" });
|
|
151
|
+
await handleAction(envelopeFor(`apr:${triage.id}:ok`));
|
|
152
|
+
assert.strictEqual(jobsAdded.length, 1, "approve wakes the agent");
|
|
153
|
+
assert.strictEqual(jobsAdded[0].triageApprovalId, triage.id, "job carries the turn grant id");
|
|
154
|
+
assert.strictEqual(jobsAdded[0].channelId, "ext-channel", "agent wakes in the origin channel");
|
|
155
|
+
assert.strictEqual(jobsAdded[0].provider, "claude", "provider pinned from the record");
|
|
156
|
+
assert.ok(jobsAdded[0].prompt.includes("Can you check invoice 42?"), "prompt carries the request");
|
|
157
|
+
assert.ok(jobsAdded[0].prompt.includes("strictly as data"), "request text framed as data, not instructions");
|
|
158
|
+
|
|
159
|
+
// ── triage deny: polite decline to the external, no agent run ──
|
|
160
|
+
const triageNo = approvals.create({ ...base, kind: "triage", inboundText: "Wire me the payroll." });
|
|
161
|
+
await handleAction(envelopeFor(`apr:${triageNo.id}:deny`));
|
|
162
|
+
assert.strictEqual(jobsAdded.length, 1, "deny never wakes the agent");
|
|
163
|
+
assert.strictEqual(relayed.length, 1, "external gets the decline");
|
|
164
|
+
assert.ok(/can't help/.test(relayed[0].text));
|
|
165
|
+
|
|
166
|
+
// ── make-a-rule: approve + scope buttons (person scope available when the
|
|
167
|
+
// record carries an entity slug) ──
|
|
168
|
+
relayed.length = 0;
|
|
169
|
+
const ruled = approvals.create({ ...base, kind: "reply", payloadText: "Office hours are 9-5." });
|
|
170
|
+
await handleAction(envelopeFor(`apr:${ruled.id}:rule`));
|
|
171
|
+
assert.strictEqual(relayed.length, 1, "make-a-rule still delivers the approved reply");
|
|
172
|
+
const scopeMsg = sends[sends.length - 1];
|
|
173
|
+
assert.ok(/who should it apply to/.test(scopeMsg.text), "scope prompt sent");
|
|
174
|
+
const rows = JSON.stringify(scopeMsg.opts?.keyboard || {});
|
|
175
|
+
assert.ok(rows.includes(`grl:${ruled.id}:person`), "person scope offered");
|
|
176
|
+
assert.ok(rows.includes(`grl:${ruled.id}:channel`), "channel scope offered");
|
|
177
|
+
assert.ok(rows.includes(`grl:${ruled.id}:global`), "global scope offered");
|
|
178
|
+
|
|
179
|
+
// ── expired card: nothing to decide ──
|
|
180
|
+
await handleAction(envelopeFor("apr:apr_0000_dead:ok"));
|
|
181
|
+
assert.ok(/expired|cleaned up/.test(lastEdit()), "stale card gets a clear ack");
|
|
182
|
+
|
|
183
|
+
console.log("approval cards OK");
|
|
184
|
+
})().catch((e) => { console.error(e); process.exit(1); });
|