@adrata/adrata-mcp 1.0.19 → 1.0.30
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/access/tiers.js +12 -0
- package/package.json +3 -3
- package/security.js +167 -14
- package/server.js +24 -1
- package/server.json +2 -2
- package/skills/qa-the-card/SKILL.md +8 -1
- package/tool-annotations.js +17 -0
- package/tools/provisioning/onboarding-tools.js +730 -0
- package/tools/source-control/connection-tools.js +117 -1
- package/tools/work-board-tools.js +256 -13
- package/tools/work-hub/criteria-quality.js +3 -1
package/access/tiers.js
CHANGED
|
@@ -27,6 +27,12 @@ export const TOOL_TIERS = {
|
|
|
27
27
|
// or reach a second workspace from the CLI at all.
|
|
28
28
|
list_workspaces: TIERS.ENTERPRISE,
|
|
29
29
|
switch_workspace: TIERS.ENTERPRISE,
|
|
30
|
+
// Provisioning a customer's workspace: reading who is seated, and seating
|
|
31
|
+
// somebody with the onboarding link that reaches them. Enterprise because it
|
|
32
|
+
// is workspace administration -- the API separately requires the connected
|
|
33
|
+
// user to be a workspace admin and answers 403 otherwise.
|
|
34
|
+
list_workspace_seats: TIERS.ENTERPRISE,
|
|
35
|
+
create_onboarding_link: TIERS.ENTERPRISE,
|
|
30
36
|
adrata_api_catalog: TIERS.FREE,
|
|
31
37
|
paper_app_audit: TIERS.FREE,
|
|
32
38
|
adrata_desktop_app_audit: TIERS.FREE,
|
|
@@ -220,6 +226,7 @@ export const TOOL_TIERS = {
|
|
|
220
226
|
list_work_boards: TIERS.ENTERPRISE,
|
|
221
227
|
list_source_control_connections: TIERS.ENTERPRISE,
|
|
222
228
|
get_source_control_connection_events: TIERS.ENTERPRISE,
|
|
229
|
+
bind_repository_to_board: TIERS.ENTERPRISE,
|
|
223
230
|
get_work_item_delivery_evidence: TIERS.ENTERPRISE,
|
|
224
231
|
audit_work_hub: TIERS.ENTERPRISE,
|
|
225
232
|
list_work_item_acceptance_criteria: TIERS.ENTERPRISE,
|
|
@@ -258,6 +265,11 @@ export const TOOL_TIERS = {
|
|
|
258
265
|
unsatisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
|
|
259
266
|
comment_on_work_item: TIERS.ENTERPRISE,
|
|
260
267
|
flag_work_item: TIERS.ENTERPRISE,
|
|
268
|
+
// Retiring a card and bringing it back. Same reasoning as every board write:
|
|
269
|
+
// these change a real workspace record.
|
|
270
|
+
archive_work_item: TIERS.ENTERPRISE,
|
|
271
|
+
delete_work_item: TIERS.ENTERPRISE,
|
|
272
|
+
unarchive_work_item: TIERS.ENTERPRISE,
|
|
261
273
|
// The containers above the cards, and the "add this to the roadmap" verb.
|
|
262
274
|
// Same reasoning as every board tool: scopes are real workspace records.
|
|
263
275
|
list_work_scopes: TIERS.ENTERPRISE,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrata/adrata-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM.
|
|
3
|
+
"version": "1.0.30",
|
|
4
|
+
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. About 275 tools registered at startup for companies, people, deals, actions, buyer groups, warm intros, webhooks and intelligence, plus 65 more behind eight named toolsets you load with enable_toolset.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
|
-
"test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
|
|
12
|
+
"test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js security.cap-contract.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/provisioning/onboarding-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
package/security.js
CHANGED
|
@@ -143,18 +143,169 @@ export function rateLimitResponse(retryAfterMs) {
|
|
|
143
143
|
// 2. Input Validation
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Length caps for TOP-LEVEL string tool arguments, by argument name.
|
|
148
|
+
*
|
|
149
|
+
* `applySecurityLayer` wraps every tool this server registers, so these caps
|
|
150
|
+
* are the FIRST thing any argument meets — before Zod, before the handler,
|
|
151
|
+
* before the API. Until 2026-09-05 they TRUNCATED, silently, and the tool then
|
|
152
|
+
* returned success: a card flag ended mid-word at exactly 1000 characters
|
|
153
|
+
* ("Do not substitute API SHA or bounce the card merely t") and the
|
|
154
|
+
* prohibition was gone for every subsequent reader including its author.
|
|
155
|
+
*
|
|
156
|
+
* Every comparable cap in the Rust API refuses instead — comments 2000,
|
|
157
|
+
* criterion parts 600, engineering-proof fields 600, reviewer model 120,
|
|
158
|
+
* evidence summary 600 — several enforced twice, once in the handler and
|
|
159
|
+
* again as a database CHECK. So every caller already handles a refusal. This
|
|
160
|
+
* layer was the only one that cut, and it ran first, which is why the cut was
|
|
161
|
+
* invisible from both ends.
|
|
162
|
+
*/
|
|
163
|
+
const DEFAULT_STRING_CAP = 1000;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The API's own cap on a card comment, and on every free-text `reason` that
|
|
167
|
+
* routes through `validate_body`: flag, block, dependency, sharing, QA-flow
|
|
168
|
+
* note. Rust calls it `MAX_COMMENT_BODY_CHARS`
|
|
169
|
+
* (`code/api/crates/routes-gtm/src/work_boards/mod.rs`).
|
|
170
|
+
*
|
|
171
|
+
* `security.cap-contract.test.js` reads that constant out of the Rust
|
|
172
|
+
* source and fails if this number ever drops below it, because THAT is the
|
|
173
|
+
* divergence this whole change is about: a cap here that sits below the cap
|
|
174
|
+
* downstream cuts a value the API would have accepted, and the API never gets
|
|
175
|
+
* to refuse it.
|
|
176
|
+
*/
|
|
177
|
+
const API_COMMENT_BODY_CAP = 2000;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sanity ceiling for an argument that carries a document or a data file rather
|
|
181
|
+
* than prose (a CSV to import, a knowledge file's markdown, a paper's HTML).
|
|
182
|
+
* Deliberately far above any product rule: the point is to bound the transport,
|
|
183
|
+
* and to let the API be the layer that says how big a document may be.
|
|
184
|
+
*/
|
|
185
|
+
const BULK_PAYLOAD_CAP = 1_000_000;
|
|
186
|
+
|
|
187
|
+
const STRING_CAPS_BY_KEY = {
|
|
188
|
+
// Long-form prose the API caps far lower anyway (a comment body is refused
|
|
189
|
+
// at 2000). 10000 here is a sanity ceiling, not the product rule.
|
|
190
|
+
body: 10000,
|
|
191
|
+
message: 10000,
|
|
192
|
+
description: 10000,
|
|
193
|
+
|
|
194
|
+
// The reason family. These were the DANGEROUS truncations and the reason
|
|
195
|
+
// this cap exists as its own class:
|
|
196
|
+
//
|
|
197
|
+
// flag_work_item.reason API cap 2000 — a 1000-cut value was
|
|
198
|
+
// ACCEPTED truncated. This is the flag
|
|
199
|
+
// defect: a prohibition ended mid-word at
|
|
200
|
+
// exactly 1000 characters and read as
|
|
201
|
+
// complete to every later reader.
|
|
202
|
+
// block_work_item.reason API cap 2000 — same shape.
|
|
203
|
+
// move_work_item.reason NO API cap at all, so nothing downstream
|
|
204
|
+
// transfer_...between_boards could ever catch the cut. Permanently and
|
|
205
|
+
// silently shortened to 1000.
|
|
206
|
+
// every X-Adrata-Reason header NO API cap anywhere in Rust
|
|
207
|
+
// (api-bridge.js) (presence-only). Same silence.
|
|
208
|
+
//
|
|
209
|
+
// Raising these to the API's own 2000 is what keeps this change from
|
|
210
|
+
// trading a silent cut for a refusal of honest input: a 1400-character
|
|
211
|
+
// reason is legitimate, the API accepts it, and until now this layer cut it.
|
|
212
|
+
reason: API_COMMENT_BODY_CAP,
|
|
213
|
+
|
|
214
|
+
// `auditReason` is the same field family under a different name: the shared
|
|
215
|
+
// `moneyWrite` envelope (server.js) spreads it into EVERY money write, so it
|
|
216
|
+
// was the one reason-shaped argument the first version of this table missed.
|
|
217
|
+
auditReason: API_COMMENT_BODY_CAP,
|
|
218
|
+
|
|
219
|
+
// `record_work_item_qa_failure_and_release.details` is composed into a flag
|
|
220
|
+
// reason (`"QA criterion failure (<id>): " + details`) and so lands under
|
|
221
|
+
// the same 2000 API cap. A 1000-character cut plus that ~14-character prefix
|
|
222
|
+
// stayed comfortably under 2000, which is exactly why the "~1,014 characters,
|
|
223
|
+
// 200 OK" measurement was silent. Known rough edge: a `details` near 2000
|
|
224
|
+
// composes to over 2000 and the API refuses the composed value with its own
|
|
225
|
+
// message. That is a refusal rather than a silence, which is the property
|
|
226
|
+
// this change is about.
|
|
227
|
+
details: API_COMMENT_BODY_CAP,
|
|
228
|
+
|
|
229
|
+
// Search terms. A query longer than this is a paste, not a search.
|
|
230
|
+
query: 500,
|
|
231
|
+
search: 500,
|
|
232
|
+
|
|
233
|
+
// --- Payloads, not prose ---
|
|
234
|
+
//
|
|
235
|
+
// These carry a document or a data file, and at the old 1000 default they
|
|
236
|
+
// were the WORST instances of the truncation: `bulk_import`/`manage_data`
|
|
237
|
+
// took a CSV — any real one is past 1000 in its first few rows — cut it to
|
|
238
|
+
// 1000, imported the fragment, and reported success. A partial import that
|
|
239
|
+
// announces itself as complete is harder to notice than a lost sentence.
|
|
240
|
+
//
|
|
241
|
+
// The ceiling here is a transport sanity bound, NOT a product rule. This
|
|
242
|
+
// layer has no business deciding how large a knowledge file may be; the API
|
|
243
|
+
// owns that, and it can refuse. What this layer must never again do is
|
|
244
|
+
// decide it silently.
|
|
245
|
+
csvData: BULK_PAYLOAD_CAP,
|
|
246
|
+
content: BULK_PAYLOAD_CAP,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** The cap that applies to a top-level string argument named `key`. */
|
|
250
|
+
export function maxLengthForKey(key) {
|
|
251
|
+
return Object.prototype.hasOwnProperty.call(STRING_CAPS_BY_KEY, key)
|
|
252
|
+
? STRING_CAPS_BY_KEY[key]
|
|
253
|
+
: DEFAULT_STRING_CAP;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The refusal text. It names the actual length, the cap, and how much to cut,
|
|
258
|
+
* because the caller is usually an agent that can shorten and retry — and it
|
|
259
|
+
* says plainly that nothing was written, since the whole point of the defect
|
|
260
|
+
* this replaces was a caller believing a truncated write had landed.
|
|
261
|
+
*/
|
|
262
|
+
export function overLengthMessage(field, length, maxLength) {
|
|
263
|
+
return `Parameter "${field}" is ${length} characters and the cap is ${maxLength} — `
|
|
264
|
+
+ `remove ${length - maxLength}. Nothing was written: the value was refused whole `
|
|
265
|
+
+ `rather than cut, so shorten it and call again.`;
|
|
266
|
+
}
|
|
267
|
+
|
|
146
268
|
/**
|
|
147
269
|
* Common validation schemas for tool parameters.
|
|
148
270
|
* These wrap and tighten the existing Zod schemas.
|
|
149
271
|
*/
|
|
150
272
|
export const validators = {
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Strip control characters, and REFUSE a string longer than `maxLength`.
|
|
275
|
+
*
|
|
276
|
+
* Returns `{ value, error }` rather than a bare string on purpose. Until
|
|
277
|
+
* 2026-09-05 this ended `return cleaned.slice(0, maxLength)` — the value was
|
|
278
|
+
* cut, `validateToolInput` still reported `valid: true`, and the write went
|
|
279
|
+
* on to return 200. The object shape means a caller cannot use the result as
|
|
280
|
+
* a string without noticing, so the refusal cannot be dropped the way the
|
|
281
|
+
* silent slice was.
|
|
282
|
+
*
|
|
283
|
+
* Length is counted in CODE POINTS (`[...cleaned].length`), matching the
|
|
284
|
+
* Rust API's `chars().count()` downstream rather than UTF-16 code units, so
|
|
285
|
+
* a value this layer accepts is not one the next layer measures differently.
|
|
286
|
+
*/
|
|
287
|
+
sanitizeString(value, maxLength = DEFAULT_STRING_CAP, field = 'value') {
|
|
288
|
+
if (typeof value !== 'string') return { value, error: null };
|
|
289
|
+
// Vertical tab (0x0B) and form feed (0x0C) ARE whitespace: they separate
|
|
290
|
+
// words. Deleting them outright fuses the tokens either side into one —
|
|
291
|
+
// "evidence<VT>01M1RAMY" becomes "evidence01M1RAMY" — and that is worse to
|
|
292
|
+
// read than a truncation, because the sentence still looks complete, so a
|
|
293
|
+
// reader has no tell at all. They collapse to a space. Every other control
|
|
294
|
+
// character in the class is not a separator, so removing it stays correct.
|
|
295
|
+
//
|
|
296
|
+
// Tab (0x09), newline (0x0A) and carriage return (0x0D) sit outside both
|
|
297
|
+
// classes and pass through untouched, as they always have.
|
|
298
|
+
const cleaned = value
|
|
299
|
+
// eslint-disable-next-line no-control-regex
|
|
300
|
+
.replace(/[\x0B\x0C]/g, ' ')
|
|
301
|
+
// eslint-disable-next-line no-control-regex
|
|
302
|
+
.replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '');
|
|
303
|
+
// Measured AFTER stripping: control characters must not eat the budget.
|
|
304
|
+
const length = [...cleaned].length;
|
|
305
|
+
if (length > maxLength) {
|
|
306
|
+
return { value: cleaned, error: overLengthMessage(field, length, maxLength) };
|
|
307
|
+
}
|
|
308
|
+
return { value: cleaned, error: null };
|
|
158
309
|
},
|
|
159
310
|
|
|
160
311
|
/** Validate that a string doesn't contain obvious injection patterns. */
|
|
@@ -223,13 +374,15 @@ export function validateToolInput(toolName, args) {
|
|
|
223
374
|
continue;
|
|
224
375
|
}
|
|
225
376
|
|
|
226
|
-
// Sanitize string values
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
377
|
+
// Sanitize string values. An over-long value is REFUSED, not cut:
|
|
378
|
+
// see the cap table above for why this layer used to truncate silently.
|
|
379
|
+
const { value: cleaned, error: lengthError } =
|
|
380
|
+
validators.sanitizeString(value, maxLengthForKey(key), key);
|
|
381
|
+
if (lengthError) {
|
|
382
|
+
errors.push(lengthError);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
sanitized[key] = cleaned;
|
|
233
386
|
|
|
234
387
|
// Validate specific field types
|
|
235
388
|
if (key === 'id' || key.endsWith('Id')) {
|
package/server.js
CHANGED
|
@@ -55,6 +55,7 @@ import { registerEmailTools } from './tools/email-tools.js';
|
|
|
55
55
|
import { registerWorkBoardTools } from './tools/work-board-tools.js';
|
|
56
56
|
import { registerSourceControlTools } from './tools/source-control/connection-tools.js';
|
|
57
57
|
import { registerRoadmapTools } from './tools/roadmap-tools.js';
|
|
58
|
+
import { registerProvisioningTools } from './tools/provisioning/onboarding-tools.js';
|
|
58
59
|
import { registerPaperTools } from './tools/paper-tools.js';
|
|
59
60
|
import { register as registerAlwaysLoadedTools } from './toolsets/revenue/always-loaded.js';
|
|
60
61
|
import { register as registerExtensibilityTools } from './toolsets/extensibility.js';
|
|
@@ -2394,7 +2395,29 @@ registerWorkBoardTools(server, {
|
|
|
2394
2395
|
getGrantedScope: () => loadTokens()?.scope,
|
|
2395
2396
|
});
|
|
2396
2397
|
|
|
2397
|
-
registerSourceControlTools(server, {
|
|
2398
|
+
registerSourceControlTools(server, {
|
|
2399
|
+
z,
|
|
2400
|
+
api,
|
|
2401
|
+
ok,
|
|
2402
|
+
validateApiBridgeRequest,
|
|
2403
|
+
buildMutationHeaders,
|
|
2404
|
+
getGrantedScope: () => loadTokens()?.scope,
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
// Seating a customer's people and minting the onboarding link an AE hands them.
|
|
2408
|
+
// The first path from MCP that can actually finish: `invite_user` through
|
|
2409
|
+
// `adrata_ai_tool_execute` refuses every machine principal at the confirmation
|
|
2410
|
+
// step (ai_crm_tools/mod.rs:427), so the bridge could preview a seat forever and
|
|
2411
|
+
// create none. See tools/provisioning.js for why this is one tool and not two.
|
|
2412
|
+
registerProvisioningTools(server, {
|
|
2413
|
+
z,
|
|
2414
|
+
api,
|
|
2415
|
+
ok,
|
|
2416
|
+
validateApiBridgeRequest,
|
|
2417
|
+
buildMutationHeaders,
|
|
2418
|
+
getGrantedScope: () => loadTokens()?.scope,
|
|
2419
|
+
getWorkspaceId: () => loadTokens()?.workspaceId ?? AUTH?.workspaceId ?? null,
|
|
2420
|
+
});
|
|
2398
2421
|
|
|
2399
2422
|
// The containers above the cards, and the "add this to the roadmap" verb
|
|
2400
2423
|
// (company/decisions/2026-08-06-spoq-roadmap-sync.md).
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "com.adrata/adrata-mcp",
|
|
4
4
|
"description": "Adrata revenue-intelligence MCP server: companies, people, opportunities, actions, buyer groups, enrichment, email, and workspace operations for AI agents.",
|
|
5
5
|
"status": "active",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.30",
|
|
7
7
|
"websiteUrl": "https://adrata.com/developers",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/adrata/adrata",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "@adrata/adrata-mcp",
|
|
18
|
-
"version": "1.0.
|
|
18
|
+
"version": "1.0.30",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
},
|
|
@@ -13,7 +13,14 @@ The workflow semantics are fixed by
|
|
|
13
13
|
`company/decisions/2026-08-29-two-gate-ai-qa-human-production.md`; the required
|
|
14
14
|
evidence loop is fixed by
|
|
15
15
|
`company/decisions/2026-08-29-recorded-staging-qa-standard.md`. This skill is
|
|
16
|
-
their operating procedure.
|
|
16
|
+
their operating procedure. Read the current amendment at
|
|
17
|
+
`company/decisions/2026-09-04-complete-quality-gates-and-instant-interactions.md`
|
|
18
|
+
and the card contract at
|
|
19
|
+
`company/playbooks/engineering/card-readiness-and-evidence-contract.md`.
|
|
20
|
+
Every required criterion/configuration must pass: failed, blocked, skipped,
|
|
21
|
+
missing, stale, unknown, or flaky results block completion. Do not use a human
|
|
22
|
+
acknowledgement or legacy unknown-author exception to clear an incomplete gate.
|
|
23
|
+
No visible UI changes are authorized by the quality audit.
|
|
17
24
|
|
|
18
25
|
## Establish the real queue
|
|
19
26
|
|
package/tool-annotations.js
CHANGED
|
@@ -38,6 +38,13 @@ const DOMAIN_OVERRIDES = {
|
|
|
38
38
|
who_am_i: 'workspace',
|
|
39
39
|
get_current_user: 'workspace',
|
|
40
40
|
|
|
41
|
+
// Provisioning: seating a user and minting the onboarding link that reaches
|
|
42
|
+
// them. Categorised 'workspace' rather than given a category of their own —
|
|
43
|
+
// these are workspace administration, and the profile that can administer a
|
|
44
|
+
// workspace is the one that should see them.
|
|
45
|
+
list_workspace_seats: 'workspace',
|
|
46
|
+
create_onboarding_link: 'workspace',
|
|
47
|
+
|
|
41
48
|
// Governed API bridge.
|
|
42
49
|
// Broad backwards-compatible escape hatches stay on the unprofiled Adrata
|
|
43
50
|
// server. Product profiles use the namespace-bound capability bridge below.
|
|
@@ -241,10 +248,20 @@ const IDEMPOTENT_WRITES = new Set([
|
|
|
241
248
|
// comment, criterion, or card.
|
|
242
249
|
'move_work_item', 'transfer_work_item_between_boards', 'set_work_board_column_wip_limit', 'set_work_item_tag', 'set_work_item_kind',
|
|
243
250
|
'create_work_item', 'comment_on_work_item', 'flag_work_item',
|
|
251
|
+
// Retiring an already-retired card is REFUSED with a 409 rather than
|
|
252
|
+
// replayed, so the reason of the first retirement cannot be silently
|
|
253
|
+
// overwritten — which makes a retry with the same key land on the same state.
|
|
254
|
+
// The restore is a plain clear of the same five columns.
|
|
255
|
+
'archive_work_item', 'unarchive_work_item',
|
|
244
256
|
// A repeat block is the same edge (the pair is unique), and a repeat
|
|
245
257
|
// unblock deletes a row that is already gone. Neither compounds.
|
|
246
258
|
'block_work_item', 'unblock_work_item',
|
|
247
259
|
'add_work_item_acceptance_criterion',
|
|
260
|
+
// (delete_work_item needs no entry here either: DESTRUCTIVE_PATTERN matches
|
|
261
|
+
// the `delete_` prefix, so a host confirmation-gates it on its NAME. That is
|
|
262
|
+
// the whole reason the destructive branch is a separate tool rather than a
|
|
263
|
+
// `kind` argument on archive_work_item — a switch inside a safe-sounding name
|
|
264
|
+
// would be gated by nothing.)
|
|
248
265
|
// Reclassifying is a PUT of the whole routing decision, not an append:
|
|
249
266
|
// replaying the same key rewrites the criterion to the same route and the
|
|
250
267
|
// server's receipt table records one change, never a second one.
|