iit-mcp-autotask 0.29.0 → 0.30.0
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/capabilities.json +2 -2
- package/dist/access/write-env.js +6 -7
- package/dist/tools/catalog-data.d.ts +1 -1
- package/dist/tools/catalog-data.js +2 -2
- package/dist/tools/server-version.d.ts +1 -1
- package/dist/tools/server-version.js +1 -1
- package/dist/tools/server.d.ts +1 -1
- package/dist/tools/write/apply-phases.d.ts +3 -0
- package/dist/tools/write/apply-phases.js +8 -2
- package/dist/tools/write/apply.js +1 -1
- package/dist/tools/write/outcome.d.ts +2 -1
- package/dist/tools/write/outcome.js +4 -2
- package/dist/tools/write/parse.d.ts +18 -1
- package/dist/tools/write/parse.js +92 -1
- package/dist/tools/write/preview.js +18 -2
- package/dist/tools/write/snapshot.d.ts +10 -1
- package/dist/tools/write/snapshot.js +108 -6
- package/dist/write/evaluate.js +6 -2
- package/dist/write/hard-rules.d.ts +21 -9
- package/dist/write/hard-rules.js +9 -4
- package/package.json +2 -2
- package/server.json +2 -2
package/capabilities.json
CHANGED
|
@@ -340,7 +340,7 @@
|
|
|
340
340
|
},
|
|
341
341
|
{
|
|
342
342
|
"id": "autotask.generic.write",
|
|
343
|
-
"description": "Preview
|
|
343
|
+
"description": "Preview any write (create, update, delete or a composite helper) by exact key, args in declaration order, ids never names: validates, reads the target, returns a plan and diff. A composite op echoes its args only, no field diff.",
|
|
344
344
|
"mcpPrimitive": "tool",
|
|
345
345
|
"toolName": "autotask_write",
|
|
346
346
|
"type": "write",
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
"autotask_describe",
|
|
379
379
|
"autotask_read"
|
|
380
380
|
],
|
|
381
|
-
"backingRegistryOperations":
|
|
381
|
+
"backingRegistryOperations": 1279,
|
|
382
382
|
"note": "autotask_catalog finds the exact operation key (ranked search), autotask_describe serves its contract, autotask_read runs any effect=read key. Writes go through the operator profile (autotask_write then autotask_apply); authenticate.* is never served."
|
|
383
383
|
},
|
|
384
384
|
"schemas": {}
|
package/dist/access/write-env.js
CHANGED
|
@@ -12,13 +12,12 @@ const EXACT = /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9]*$/;
|
|
|
12
12
|
const FIELD = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
13
13
|
const LIMIT = /^([1-9]\d{0,5})\/(h|d)$/;
|
|
14
14
|
const TRUSTED = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
15
|
-
/** Operations the preview/apply path
|
|
15
|
+
/** Operations the preview/apply path serves: every write and destructive record the hard rules do not refuse. */
|
|
16
16
|
const servable = () => {
|
|
17
17
|
const out = new Map();
|
|
18
18
|
for (const r of RECORDS) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
out.set(r.id, shape);
|
|
19
|
+
if (hardRuleFor(r.id, r).never === undefined)
|
|
20
|
+
out.set(r.id, writeShape(r));
|
|
22
21
|
}
|
|
23
22
|
return out;
|
|
24
23
|
};
|
|
@@ -55,9 +54,9 @@ function parseWrite(raw, ops, problems) {
|
|
|
55
54
|
continue;
|
|
56
55
|
}
|
|
57
56
|
if (exact && !ops.has(pattern)) {
|
|
58
|
-
|
|
59
|
-
const never = hardRuleFor(pattern,
|
|
60
|
-
problems.push(`${pattern}: ${never ?? '
|
|
57
|
+
// every write is servable, so this is a read, a credential-minting op or an unknown key
|
|
58
|
+
const never = hardRuleFor(pattern, recordById(pattern)).never;
|
|
59
|
+
problems.push(`${pattern}: ${never ?? 'not a served write'}`);
|
|
61
60
|
continue;
|
|
62
61
|
}
|
|
63
62
|
if (exact && mode === 'allow' && hardRuleFor(pattern, recordById(pattern)).approval === 'always') {
|
|
@@ -102,7 +102,7 @@ export declare function cacheOptionsForPreset(preset: 'on' | 'off', fieldCacheTt
|
|
|
102
102
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
103
103
|
* describes the SDK behind it.
|
|
104
104
|
*/
|
|
105
|
-
export declare const BUILT_AGAINST_PLAN_HASH = "
|
|
105
|
+
export declare const BUILT_AGAINST_PLAN_HASH = "48ee23d80fad38531486c52af40f388df810d0ee4b8d368f7d18628e276578d0";
|
|
106
106
|
/**
|
|
107
107
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
108
108
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
|
@@ -126,7 +126,7 @@ export const TOOL_DESCRIPTIONS = {
|
|
|
126
126
|
autotask_contract_full_profile: 'One contract with services, billing rules, the 50 newest charges and service units. Monthly P&L: autotask_read (guide: contract-profitability).',
|
|
127
127
|
autotask_quote_get: 'One quote with its line items in one call. A list of quotes: autotask_read quotes.query.',
|
|
128
128
|
autotask_invoice_get: 'One invoice: the record (line data rides on it), or the html, xml or pdf export (format; in-band as a base64 document, or raw to output_path on stdio). A list: autotask_read invoices.query.',
|
|
129
|
-
autotask_write: 'Preview
|
|
129
|
+
autotask_write: 'Preview any write (create, update, delete or a composite helper) by exact key, args in declaration order, ids never names: validates, reads the target, returns a plan and diff. A composite op echoes its args only, no field diff.',
|
|
130
130
|
autotask_apply: 'Execute exactly one previewed plan (plan handle from autotask_write). Re-applying is safe: a recorded outcome replays. After an unknown outcome re-read the target; never re-apply blindly.',
|
|
131
131
|
};
|
|
132
132
|
/** The served description for one registered tool. */
|
|
@@ -164,7 +164,7 @@ export function cacheOptionsForPreset(preset, fieldCacheTtlMs) {
|
|
|
164
164
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
165
165
|
* describes the SDK behind it.
|
|
166
166
|
*/
|
|
167
|
-
export const BUILT_AGAINST_PLAN_HASH = '
|
|
167
|
+
export const BUILT_AGAINST_PLAN_HASH = '48ee23d80fad38531486c52af40f388df810d0ee4b8d368f7d18628e276578d0';
|
|
168
168
|
/**
|
|
169
169
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
170
170
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The wire identity of this server: a leaf module (no imports) so any tool can read it without an import cycle. */
|
|
2
2
|
export const SERVER_NAME = 'iit-mcp-autotask';
|
|
3
|
-
export const SERVER_VERSION = '0.
|
|
3
|
+
export const SERVER_VERSION = '0.30.0';
|
|
4
4
|
//# sourceMappingURL=server-version.js.map
|
package/dist/tools/server.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export declare function serverInstructions(config: Pick<Config, 'AUTOTASK_PROFIL
|
|
|
60
60
|
/** The full identity stamp (the `/metadata` build block — the era pair, FR-2). */
|
|
61
61
|
export declare const SERVER_IDENTITY: {
|
|
62
62
|
readonly name: "iit-mcp-autotask";
|
|
63
|
-
readonly version: "0.
|
|
63
|
+
readonly version: "0.30.0";
|
|
64
64
|
readonly mcpProtocolBaseline: "2026-07-28";
|
|
65
65
|
readonly legacyProtocolFallback: "2025-11-25";
|
|
66
66
|
};
|
|
@@ -79,6 +79,8 @@ export interface Sent {
|
|
|
79
79
|
correlationId?: string;
|
|
80
80
|
manualAction?: Record<string, unknown>;
|
|
81
81
|
} | undefined;
|
|
82
|
+
/** The generic `call` shape's own SDK result (there is no shaped outcome to report instead). */
|
|
83
|
+
callResult?: unknown;
|
|
82
84
|
}
|
|
83
85
|
/** `apply_start`, then the one SDK write under the cap. Never returns a ToolReturn: the outcome is always a ledger entry. */
|
|
84
86
|
export declare function send(a: ApplyCtx, s: {
|
|
@@ -97,4 +99,5 @@ export declare function respond(a: ApplyCtx, r: {
|
|
|
97
99
|
entry: LedgerEntry;
|
|
98
100
|
error: unknown;
|
|
99
101
|
moveResult: Sent['moveResult'];
|
|
102
|
+
callResult?: unknown;
|
|
100
103
|
}): Promise<ToolReturn>;
|
|
@@ -235,6 +235,7 @@ export async function send(a, s) {
|
|
|
235
235
|
let entry;
|
|
236
236
|
let error;
|
|
237
237
|
let moveResult;
|
|
238
|
+
let callResult;
|
|
238
239
|
try {
|
|
239
240
|
// ponytail: past the cap the write keeps running and its late answer is not recorded; the
|
|
240
241
|
// outcome stays unknown (cooldown, re-read) rather than racing a second ledger update.
|
|
@@ -266,6 +267,8 @@ export async function send(a, s) {
|
|
|
266
267
|
moveResult = m === null ? undefined : { status: String(m.status), correlationId: typeof m.correlationId === 'string' ? m.correlationId : undefined, ...(m.manualAction !== undefined ? { manualAction: { action: m.manualAction.action, uiLink: m.manualAction.uiLink, reason: m.manualAction.reason } } : {}) };
|
|
267
268
|
}
|
|
268
269
|
else {
|
|
270
|
+
if (shape === 'call')
|
|
271
|
+
callResult = result;
|
|
269
272
|
const id = result?.id;
|
|
270
273
|
const createdId = typeof id === 'number' || typeof id === 'string' ? id : undefined;
|
|
271
274
|
entry = {
|
|
@@ -303,7 +306,7 @@ export async function send(a, s) {
|
|
|
303
306
|
finally {
|
|
304
307
|
clearTimeout(timer);
|
|
305
308
|
}
|
|
306
|
-
return { entry, error, moveResult };
|
|
309
|
+
return { entry, error, moveResult, ...(shape === 'call' ? { callResult: callResult ?? null } : {}) };
|
|
307
310
|
}
|
|
308
311
|
/** Record the outcome (under the op lock) and run the post-write standing check. Throws a ConfigError by design; never caught here. */
|
|
309
312
|
export function recordResult(a, entry, confirmation) {
|
|
@@ -344,13 +347,16 @@ export function recordResult(a, entry, confirmation) {
|
|
|
344
347
|
/** Answer from the recorded entry: failures, moves, and the read-backs of creates and updates. */
|
|
345
348
|
export async function respond(a, r) {
|
|
346
349
|
const { ctx, store, plan } = a;
|
|
347
|
-
const { record, shape, entry, error, moveResult } = r;
|
|
350
|
+
const { record, shape, entry, error, moveResult, callResult } = r;
|
|
348
351
|
if (entry.state !== 'succeeded') {
|
|
349
352
|
const data = error === undefined ? {} : { error: honestError(typedErrorPayload(error, ctx.correlationId, requestSecrets(ctx)), error, entry) };
|
|
350
353
|
return outcomeAnswer(plan, entry, { replayed: false, data });
|
|
351
354
|
}
|
|
352
355
|
if (shape === 'move')
|
|
353
356
|
return outcomeAnswer(plan, entry, { replayed: false, ...(moveResult !== undefined ? { data: { move: moveResult } } : {}) });
|
|
357
|
+
// The generic shape answers the operation's own result (the ledger records ids and the outcome, never a payload)
|
|
358
|
+
if (shape === 'call')
|
|
359
|
+
return outcomeAnswer(plan, entry, { replayed: false, result: callResult ?? null });
|
|
354
360
|
// A create reads its stored record back (one governed get); a failed read-back never fails the write.
|
|
355
361
|
if (shape.startsWith('create') && entry.outcome?.id !== undefined) {
|
|
356
362
|
try {
|
|
@@ -56,7 +56,7 @@ export function buildApply(ctx) {
|
|
|
56
56
|
return claimed;
|
|
57
57
|
const sent = await send(a, { confirmation, record, shape, snap, base: claimed.ok.base });
|
|
58
58
|
recordResult(a, sent.entry, confirmation);
|
|
59
|
-
return respond(a, { record, shape, entry: sent.entry, error: sent.error, moveResult: sent.moveResult });
|
|
59
|
+
return respond(a, { record, shape, entry: sent.entry, error: sent.error, moveResult: sent.moveResult, callResult: sent.callResult });
|
|
60
60
|
});
|
|
61
61
|
},
|
|
62
62
|
};
|
|
@@ -14,9 +14,10 @@ export declare function notPersistedAnswer(plan: PlanRecord, entry: LedgerEntry,
|
|
|
14
14
|
record?: unknown;
|
|
15
15
|
unrecorded?: boolean;
|
|
16
16
|
}): ToolReturn;
|
|
17
|
-
/** The envelope for a recorded (or just recorded) outcome. */
|
|
17
|
+
/** The envelope for a recorded (or just recorded) outcome. `result` overrides the ledger id a success reports. */
|
|
18
18
|
export declare function outcomeAnswer(plan: PlanRecord, entry: LedgerEntry, extra: {
|
|
19
19
|
replayed: boolean;
|
|
20
20
|
data?: Record<string, unknown>;
|
|
21
21
|
summary?: string;
|
|
22
|
+
result?: unknown;
|
|
22
23
|
}): ToolReturn;
|
|
@@ -30,7 +30,7 @@ export function notPersistedAnswer(plan, entry, fields, extra) {
|
|
|
30
30
|
notPersisted: fields,
|
|
31
31
|
}, { plan: plan.handle, planHash: plan.planHash, ...(extra.replayed ? { replayed: true } : {}), summary: `${plan.op} reached the vendor (HTTP success) but ${fields.join(', ')} did not change.${extra.unrecorded === true ? ' This miss could not be recorded (the state lock was busy): a replay of this plan will answer plain ok, so re-read the record before relying on it.' : ''}` });
|
|
32
32
|
}
|
|
33
|
-
/** The envelope for a recorded (or just recorded) outcome. */
|
|
33
|
+
/** The envelope for a recorded (or just recorded) outcome. `result` overrides the ledger id a success reports. */
|
|
34
34
|
export function outcomeAnswer(plan, entry, extra) {
|
|
35
35
|
const meta = { plan: plan.handle, planHash: plan.planHash, ...(extra.replayed ? { replayed: true } : {}) };
|
|
36
36
|
const base = { operation: plan.op, targets: plan.targets, outcome: entry.state === 'sending' ? 'unknown' : entry.state, ...extra.data };
|
|
@@ -48,7 +48,9 @@ export function outcomeAnswer(plan, entry, extra) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
if (entry.state === 'succeeded') {
|
|
51
|
-
|
|
51
|
+
// a generic `call` op reports its own SDK result; a shaped one reports the ledger id
|
|
52
|
+
const result = 'result' in extra ? extra.result : entry.outcome?.id === undefined ? {} : { id: entry.outcome.id };
|
|
53
|
+
return answer('ok', 'proceed', [], { ...base, result }, { ...meta, summary: extra.summary ?? `${plan.op} succeeded${extra.replayed ? ' (recorded outcome replayed; nothing was sent again)' : ''}.` });
|
|
52
54
|
}
|
|
53
55
|
if (entry.outcome?.code === 'MOVE_RESOLVED') {
|
|
54
56
|
// issue #131: a person cleared this move by hand; a copy may exist, so never say "nothing was written"
|
|
@@ -3,12 +3,29 @@ import type { Reason } from '../reasons.js';
|
|
|
3
3
|
import type { WriteShape } from '../../write/hard-rules.js';
|
|
4
4
|
import type { PlanTargets } from '../../write/store.js';
|
|
5
5
|
export declare const SHAPE_ARGS: Record<WriteShape, string[]>;
|
|
6
|
+
/** The argument names of the generic `call` shape that name the record's own id (the snapshot's target). */
|
|
7
|
+
export declare const CALL_ID_ARGS: ReadonlySet<string>;
|
|
8
|
+
/** The argument names of the generic `call` shape that name a company the write touches (a scope fact). */
|
|
9
|
+
export declare const CALL_COMPANY_ARGS: ReadonlySet<string>;
|
|
10
|
+
/** A positive integer id, or a canonical integer string (the registry accepts both forms for ids). */
|
|
11
|
+
export declare function asId(value: unknown): number | undefined;
|
|
6
12
|
export interface ParsedArgs {
|
|
7
13
|
args: unknown[];
|
|
8
14
|
targets: PlanTargets;
|
|
9
15
|
data?: Record<string, unknown>;
|
|
10
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The generic `call` shape (issue #191): the record's own declared arguments, in registry order, sent
|
|
19
|
+
* to the SDK op unchanged. There is no field-level snapshot to validate against, so the arguments are
|
|
20
|
+
* checked by the SDK's OWN pre-request validator (`validateDispatchArgs`: arity, required args, loose
|
|
21
|
+
* types, zero wire) and by the same payload hygiene the data object gets. Ids stay ids where the
|
|
22
|
+
* registry names one; an object argument is a payload and its references are scope facts.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseCallArgs(record: CapabilityRecord, raw: unknown[]): ParsedArgs | Reason;
|
|
11
25
|
/** Validate the positional args against the shape; ids are positive integers, never names. */
|
|
12
26
|
export declare function parseArgs(shape: WriteShape, record: CapabilityRecord, raw: unknown[]): ParsedArgs | Reason;
|
|
13
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The ledger's key for a target: the record for an update or delete, the payload for a create, and for
|
|
29
|
+
* the generic `call` shape the record (or parent) it names — or, when it names none, the arguments.
|
|
30
|
+
*/
|
|
14
31
|
export declare function targetKeyOf(shape: WriteShape, targets: PlanTargets, args: unknown[]): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Argument parsing and the target key of a write (issue #89). */
|
|
2
|
+
import { validateDispatchArgs } from 'node-autotask/operations';
|
|
2
3
|
import { stripUntrustedDeep } from 'node-autotask/untrusted';
|
|
3
4
|
import { hashOf } from '../../write/canonical.js';
|
|
4
5
|
import { reason } from './common.js';
|
|
@@ -14,9 +15,89 @@ export const SHAPE_ARGS = {
|
|
|
14
15
|
delete: ['id'],
|
|
15
16
|
delete_child: ['parentId', 'id'],
|
|
16
17
|
move: ['sourceId', 'toTicketId'],
|
|
18
|
+
// the generic shape takes the record's own declared arguments, in the registry's order (below)
|
|
19
|
+
call: [],
|
|
17
20
|
};
|
|
21
|
+
/** The argument names of the generic `call` shape that name the record's own id (the snapshot's target). */
|
|
22
|
+
export const CALL_ID_ARGS = new Set(['id', 'sourceId', 'sourceContactId']);
|
|
23
|
+
/** The argument names of the generic `call` shape that name a company the write touches (a scope fact). */
|
|
24
|
+
export const CALL_COMPANY_ARGS = new Set(['companyID', 'companyId', 'destinationCompanyId', 'destinationCompanyID']);
|
|
25
|
+
/** A positive integer id, or a canonical integer string (the registry accepts both forms for ids). */
|
|
26
|
+
export function asId(value) {
|
|
27
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
|
|
28
|
+
return value;
|
|
29
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
30
|
+
const n = Number(value);
|
|
31
|
+
return Number.isSafeInteger(n) && n > 0 ? n : undefined;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The generic `call` shape (issue #191): the record's own declared arguments, in registry order, sent
|
|
37
|
+
* to the SDK op unchanged. There is no field-level snapshot to validate against, so the arguments are
|
|
38
|
+
* checked by the SDK's OWN pre-request validator (`validateDispatchArgs`: arity, required args, loose
|
|
39
|
+
* types, zero wire) and by the same payload hygiene the data object gets. Ids stay ids where the
|
|
40
|
+
* registry names one; an object argument is a payload and its references are scope facts.
|
|
41
|
+
*/
|
|
42
|
+
export function parseCallArgs(record, raw) {
|
|
43
|
+
const names = Object.keys(record.inputSchema);
|
|
44
|
+
const stripped = stripUntrustedDeep(raw);
|
|
45
|
+
if (stripped.length > names.length) {
|
|
46
|
+
return reason('INPUT_INVALID', `${record.id} takes at most ${names.length} arg(s): [${names.join(', ')}]`, 'args');
|
|
47
|
+
}
|
|
48
|
+
const required = names.filter((n) => record.inputSchema[n]?.required === true);
|
|
49
|
+
if (stripped.length < required.length) {
|
|
50
|
+
return reason('INPUT_INVALID', `${record.id} requires ${required.length} arg(s): [${required.join(', ')}] (declared: [${names.join(', ')}])`, 'args');
|
|
51
|
+
}
|
|
52
|
+
const out = [];
|
|
53
|
+
const targets = { resource: record.resource };
|
|
54
|
+
for (const [i, name] of names.entries()) {
|
|
55
|
+
const value = stripped[i];
|
|
56
|
+
if (value === undefined) {
|
|
57
|
+
out.push(undefined);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const where = `args.${i}`;
|
|
61
|
+
const payload = value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
if (payload) {
|
|
63
|
+
const dup = caseDuplicate(value, where);
|
|
64
|
+
if (dup !== undefined)
|
|
65
|
+
return reason('INPUT_INVALID', `${dup}: payload keys are plain ASCII field names (letters, digits, underscore), one key per field whatever its case`, dup);
|
|
66
|
+
const copied = sanitisedArtefact(value, where);
|
|
67
|
+
if (copied !== undefined)
|
|
68
|
+
return reason('INPUT_INVALID', `${copied} carries a copied-artefact class: a typed HTML entity sequence (< > &), a truncation marker (server or note_chars form), or a wrapper tag in this server's exact spelling. Leave it out or send a new value.`, copied);
|
|
69
|
+
}
|
|
70
|
+
const cleaned = cleanPayload(value);
|
|
71
|
+
if (payload) {
|
|
72
|
+
const leftover = leftoverCombiningMark(cleaned, where);
|
|
73
|
+
if (leftover !== undefined) {
|
|
74
|
+
return reason('INPUT_INVALID', `${leftover} carries a combining mark with no precomposed form (for example a lone U+0301); the vendor stores such a character as a different one, so the write would not round-trip. Send the precomposed character instead`, leftover);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
out.push(cleaned);
|
|
78
|
+
const id = asId(cleaned);
|
|
79
|
+
if (id !== undefined && CALL_ID_ARGS.has(name))
|
|
80
|
+
targets.id = id;
|
|
81
|
+
else if (id !== undefined && name === 'parentId')
|
|
82
|
+
targets.parentId = id;
|
|
83
|
+
}
|
|
84
|
+
// a trailing absent option is simply absent (a stored `undefined` would read back as null)
|
|
85
|
+
let last = out.length - 1;
|
|
86
|
+
while (last >= 0 && out[last] === undefined)
|
|
87
|
+
last--;
|
|
88
|
+
const args = out.slice(0, last + 1);
|
|
89
|
+
try {
|
|
90
|
+
validateDispatchArgs(record, args);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return reason('INPUT_INVALID', `${record.id}: ${err.message}`, 'args');
|
|
94
|
+
}
|
|
95
|
+
return { args, targets };
|
|
96
|
+
}
|
|
18
97
|
/** Validate the positional args against the shape; ids are positive integers, never names. */
|
|
19
98
|
export function parseArgs(shape, record, raw) {
|
|
99
|
+
if (shape === 'call')
|
|
100
|
+
return parseCallArgs(record, raw);
|
|
20
101
|
const names = SHAPE_ARGS[shape];
|
|
21
102
|
if (raw.length !== names.length)
|
|
22
103
|
return reason('INPUT_INVALID', `${record.id} takes ${names.length} arg(s): [${names.join(', ')}]`, 'args');
|
|
@@ -60,10 +141,20 @@ export function parseArgs(shape, record, raw) {
|
|
|
60
141
|
}
|
|
61
142
|
return data === undefined ? { args: out, targets } : { args: out, targets, data };
|
|
62
143
|
}
|
|
63
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* The ledger's key for a target: the record for an update or delete, the payload for a create, and for
|
|
146
|
+
* the generic `call` shape the record (or parent) it names — or, when it names none, the arguments.
|
|
147
|
+
*/
|
|
64
148
|
export function targetKeyOf(shape, targets, args) {
|
|
65
149
|
if (shape.startsWith('create'))
|
|
66
150
|
return `${targets.resource}:create:${hashOf(args).slice(7, 23)}`;
|
|
151
|
+
if (shape === 'call') {
|
|
152
|
+
if (targets.id !== undefined)
|
|
153
|
+
return `${targets.resource}:${targets.parentId === undefined ? '' : `${targets.parentId}/`}${targets.id}`;
|
|
154
|
+
if (targets.parentId !== undefined)
|
|
155
|
+
return `${targets.resource}:parent:${targets.parentId}`;
|
|
156
|
+
return `${targets.resource}:call:${hashOf(args).slice(7, 23)}`;
|
|
157
|
+
}
|
|
67
158
|
return `${targets.resource}:${targets.parentId === undefined ? '' : `${targets.parentId}/`}${targets.id}`;
|
|
68
159
|
}
|
|
69
160
|
//# sourceMappingURL=parse.js.map
|
|
@@ -11,6 +11,17 @@ import { parseArgs } from './parse.js';
|
|
|
11
11
|
import { honestImpact } from './payload.js';
|
|
12
12
|
import { takeSnapshot, moveSummary } from './snapshot.js';
|
|
13
13
|
import { unresolvedReason, markedDiff, deriveFacts } from './facts.js';
|
|
14
|
+
/**
|
|
15
|
+
* The generic `call` shape has no field-level snapshot (issue #191): no vendor reads establish what the
|
|
16
|
+
* op will change, so the preview states the arguments it will send — unchanged — and an impact that
|
|
17
|
+
* claims nothing it cannot know. It is one call, and this server cannot undo it.
|
|
18
|
+
*/
|
|
19
|
+
// A composite op decides its own steps, so the preview must not claim a record
|
|
20
|
+
// count or a single-record scope: `affected: null` + scope 'composite' is the
|
|
21
|
+
// honest answer (operations.recovery can delete up to `limit` rows;
|
|
22
|
+
// resources.transferOwnership moves a whole book of work).
|
|
23
|
+
const CALL_IMPACT = { affected: null, scope: 'composite', reversible: false };
|
|
24
|
+
const CALL_WARNING = 'this operation has no field-level snapshot: the preview echoes the arguments as they will be sent and the scope reads behind them; autotask_apply sends exactly these arguments to the SDK operation. The blast radius is the op own: it can affect more than one record - check the operation arguments';
|
|
14
25
|
/** Blocked state, the hard rules and the AUTOTASK_WRITE row, then the args: each answers before any read. */
|
|
15
26
|
function admit(r) {
|
|
16
27
|
const { ctx, store, loaded, args, op, audit } = r;
|
|
@@ -80,7 +91,11 @@ async function snapshot(r, a) {
|
|
|
80
91
|
/** The SDK dry run: its validation and the diff (no vendor write is issued). */
|
|
81
92
|
async function dryRun(r, a) {
|
|
82
93
|
const { ctx, op, audit, signal } = r;
|
|
83
|
-
const { record, parsed } = a;
|
|
94
|
+
const { record, parsed, shape } = a;
|
|
95
|
+
// The generic shape has no dry run to ask for (the op decides its own steps): the preview answers
|
|
96
|
+
// from the arguments and the snapshot, and the apply is the one call that sends them.
|
|
97
|
+
if (shape === 'call')
|
|
98
|
+
return { ok: { wouldApply: true, warnings: [CALL_WARNING] } };
|
|
84
99
|
const confirm = record.effect === 'destructive' || record.flags.includes('requiresApproval') ? { confirm: op } : {};
|
|
85
100
|
const invoke = record.effect === 'destructive' ? ctx.ops.invokeDelete : ctx.ops.invokeWrite;
|
|
86
101
|
let dry;
|
|
@@ -143,7 +158,8 @@ function evaluatePhase(r, a, snap, dry, resource) {
|
|
|
143
158
|
hazards: decision.hazards,
|
|
144
159
|
diff: markedDiff(dry.diff),
|
|
145
160
|
warnings: (dry.warnings ?? []).slice(0, 20).map((x) => String(x)),
|
|
146
|
-
impact: honestImpact(dry.impact, shape),
|
|
161
|
+
impact: shape === 'call' ? CALL_IMPACT : honestImpact(dry.impact, shape),
|
|
162
|
+
...(shape === 'call' ? { dispatch: 'sdk-op', args: parsed.args } : {}),
|
|
147
163
|
...(resource !== undefined ? { resource } : {}),
|
|
148
164
|
...(shape === 'move' ? { move: moveSummary(record, parsed.targets, snap.current, dry.data, facts) } : {}),
|
|
149
165
|
};
|
|
@@ -3,7 +3,7 @@ import type { ToolContext } from '../context.js';
|
|
|
3
3
|
import type { Reason } from '../reasons.js';
|
|
4
4
|
import type { WriteFacts } from '../../write/evaluate.js';
|
|
5
5
|
import type { PlanTargets } from '../../write/store.js';
|
|
6
|
-
import type
|
|
6
|
+
import { type ParsedArgs } from './parse.js';
|
|
7
7
|
/** id references that lead to a record's company when it has no companyID of its own. */
|
|
8
8
|
export declare const ANCHORS: ReadonlyArray<[string, string]>;
|
|
9
9
|
/**
|
|
@@ -36,6 +36,15 @@ export interface Snapshot {
|
|
|
36
36
|
duplicateIds: unknown[];
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The generic `call` snapshot (issue #191): there is no known target/data signature, so the record's
|
|
41
|
+
* own declared arguments decide what is read. A named id (`id`, `sourceId`, `sourceContactId`) is the
|
|
42
|
+
* record's target, read and anchored; `parentId` is the parent record; a named company id is a scope
|
|
43
|
+
* fact; any object argument is a payload, and its references are scope facts (exactly as a create's
|
|
44
|
+
* data). An argument set that names no record yields no scope facts, and a company-scoped run scope
|
|
45
|
+
* then refuses the write (a write whose company cannot be established is refused).
|
|
46
|
+
*/
|
|
47
|
+
export declare function takeCallSnapshot(ctx: ToolContext, record: CapabilityRecord, parsed: ParsedArgs, signal: AbortSignal | undefined): Promise<Snapshot | Reason>;
|
|
39
48
|
export declare function takeSnapshot(ctx: ToolContext, record: CapabilityRecord, parsed: ParsedArgs, duplicateFields: readonly string[] | undefined, signal: AbortSignal | undefined): Promise<Snapshot | Reason>;
|
|
40
49
|
/** The preview block of a move (ids, fact names and field NAMES only: note text never rides it). */
|
|
41
50
|
export declare function moveSummary(record: CapabilityRecord, t: PlanTargets, current: Record<string, unknown> | undefined, payload: unknown, facts: WriteFacts): Record<string, unknown>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** The pre-write snapshot (the before state), the stable facts and the forced-duplicate filters (issue #89). */
|
|
2
2
|
import { relationshipMap } from 'node-autotask/operations';
|
|
3
3
|
import { hashOf } from '../../write/canonical.js';
|
|
4
|
+
import { writeShape } from '../../write/hard-rules.js';
|
|
4
5
|
import { Cancelled, reason, isNotFound } from './common.js';
|
|
6
|
+
import { CALL_COMPANY_ARGS, CALL_ID_ARGS, asId } from './parse.js';
|
|
5
7
|
import { ci } from './payload.js';
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
7
9
|
// The snapshot: fresh reads, the companies the write touches, the duplicate query
|
|
@@ -45,15 +47,16 @@ export function hasSimpleGet(resource, ctx) {
|
|
|
45
47
|
export function getIds(ctx, resource, parentId, id) {
|
|
46
48
|
return parentId !== undefined && !hasSimpleGet(resource, ctx) ? [parentId, id] : [id];
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
/** The reader for one snapshot. Only the write's own target record is hashed in full; a referenced record is hashed by its stable facts. */
|
|
51
|
+
function createReader(ctx, targetKey, signal) {
|
|
50
52
|
const reads = {};
|
|
53
|
+
const hashed = {};
|
|
54
|
+
const companies = new Set();
|
|
55
|
+
const unresolved = [];
|
|
51
56
|
// The write's own TARGET record is hashed in full (an update/delete must notice any change to it);
|
|
52
57
|
// a record that is only REFERENCED (the ticket a time entry is logged on) is hashed by its stable
|
|
53
58
|
// facts only, so a volatile stamp the first of two outstanding writes moves (lastActivityDate,
|
|
54
59
|
// modified stamps) does not read as drift for the second (issue #108).
|
|
55
|
-
const targetKey = parsed.targets.id === undefined ? undefined : `${record.resource}:${getIds(ctx, record.resource, parsed.targets.parentId, parsed.targets.id).join('/')}`;
|
|
56
|
-
const hashed = {};
|
|
57
60
|
const get = async (resource, ids) => {
|
|
58
61
|
const key = `${resource}:${ids.join('/')}`;
|
|
59
62
|
if (!(key in reads)) {
|
|
@@ -65,8 +68,6 @@ export async function takeSnapshot(ctx, record, parsed, duplicateFields, signal)
|
|
|
65
68
|
}
|
|
66
69
|
return reads[key];
|
|
67
70
|
};
|
|
68
|
-
const companies = new Set();
|
|
69
|
-
const unresolved = [];
|
|
70
71
|
// The positional ids for one reference's get; a parent-scoped get needs the parent id from the same record.
|
|
71
72
|
const idsFor = async (resource, id, rec) => {
|
|
72
73
|
if (hasSimpleGet(resource, ctx))
|
|
@@ -116,6 +117,107 @@ export async function takeSnapshot(ctx, record, parsed, duplicateFields, signal)
|
|
|
116
117
|
await anchor(res, child, depth + 1, false);
|
|
117
118
|
}
|
|
118
119
|
};
|
|
120
|
+
return { reads, hashed, companies, unresolved, get, idsFor, anchor };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The generic `call` snapshot (issue #191): there is no known target/data signature, so the record's
|
|
124
|
+
* own declared arguments decide what is read. A named id (`id`, `sourceId`, `sourceContactId`) is the
|
|
125
|
+
* record's target, read and anchored; `parentId` is the parent record; a named company id is a scope
|
|
126
|
+
* fact; any object argument is a payload, and its references are scope facts (exactly as a create's
|
|
127
|
+
* data). An argument set that names no record yields no scope facts, and a company-scoped run scope
|
|
128
|
+
* then refuses the write (a write whose company cannot be established is refused).
|
|
129
|
+
*/
|
|
130
|
+
export async function takeCallSnapshot(ctx, record, parsed, signal) {
|
|
131
|
+
ctx.ops.freshen(ctx.client);
|
|
132
|
+
const { targets } = parsed;
|
|
133
|
+
const names = Object.keys(record.inputSchema);
|
|
134
|
+
const parent = await parentOf(record.resource);
|
|
135
|
+
// `parentId` names the parent record when the resource HAS a parent (a child collection); on a
|
|
136
|
+
// resource with no parent it is the record's own id (inventory_products.stockedItemsAdd's parentId
|
|
137
|
+
// IS the product). Either way it is the row this preview reads and hashes in full.
|
|
138
|
+
const selfId = targets.id ?? (parent === undefined ? targets.parentId : undefined);
|
|
139
|
+
const r = createReader(ctx, selfId === undefined ? undefined : `${record.resource}:${selfId}`, signal);
|
|
140
|
+
let current;
|
|
141
|
+
if (selfId !== undefined) {
|
|
142
|
+
const simple = hasSimpleGet(record.resource, ctx);
|
|
143
|
+
const key = `${record.resource}:${selfId}`;
|
|
144
|
+
try {
|
|
145
|
+
if (!simple && ctx.record(`${record.resource}.query`) !== undefined) {
|
|
146
|
+
// a parent-scoped get needs a parent the arguments do not name (a contact's companyID): read the
|
|
147
|
+
// row by id, as the move path does, and let the row's own references anchor it
|
|
148
|
+
const found = await ctx.ops.invokeRead(ctx.client, `${record.resource}.query`, [{ filter: [{ field: 'id', op: 'eq', value: selfId }], limit: 1 }], undefined, signal);
|
|
149
|
+
if (found === undefined)
|
|
150
|
+
throw new Cancelled();
|
|
151
|
+
const row = (found.items ?? [])[0];
|
|
152
|
+
if (row === null || typeof row !== 'object')
|
|
153
|
+
return reason('INPUT_INVALID', `${record.resource} ${selfId} does not exist`, 'args.0');
|
|
154
|
+
r.reads[key] = row;
|
|
155
|
+
r.hashed[key] = row;
|
|
156
|
+
current = row;
|
|
157
|
+
}
|
|
158
|
+
else if (simple) {
|
|
159
|
+
current = await r.get(record.resource, getIds(ctx, record.resource, targets.parentId, selfId));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return reason('INPUT_INVALID', `${record.resource} ${selfId}: this resource's get needs a parent id and the arguments do not name one`, 'args.0');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (!isNotFound(err))
|
|
167
|
+
throw err;
|
|
168
|
+
return reason('INPUT_INVALID', `${record.resource} ${selfId} does not exist`, 'args.0');
|
|
169
|
+
}
|
|
170
|
+
await r.anchor(record.resource, current, 0, false);
|
|
171
|
+
}
|
|
172
|
+
for (const [i, name] of names.entries()) {
|
|
173
|
+
const value = parsed.args[i];
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
continue;
|
|
176
|
+
const id = asId(value);
|
|
177
|
+
if (name === 'parentId') {
|
|
178
|
+
if (parent === undefined)
|
|
179
|
+
continue; // consumed as the record's own id above
|
|
180
|
+
const ids = id === undefined ? undefined : await r.idsFor(parent.resource, id, current ?? {});
|
|
181
|
+
if (ids === undefined)
|
|
182
|
+
r.unresolved.push(`${record.resource}.parent`);
|
|
183
|
+
else {
|
|
184
|
+
try {
|
|
185
|
+
await r.anchor(parent.resource, await r.get(parent.resource, ids), 1, false);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (!isNotFound(err))
|
|
189
|
+
throw err;
|
|
190
|
+
r.unresolved.push(`${record.resource}.${parent.field}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (CALL_ID_ARGS.has(name))
|
|
196
|
+
continue;
|
|
197
|
+
if (CALL_COMPANY_ARGS.has(name)) {
|
|
198
|
+
if (id !== undefined)
|
|
199
|
+
r.companies.add(id);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
203
|
+
await r.anchor(record.resource, value, 0, true);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
hash: hashOf({ reads: r.hashed }),
|
|
207
|
+
companyIds: [...r.companies].sort((a, b) => a - b),
|
|
208
|
+
unresolved: [...new Set(r.unresolved)],
|
|
209
|
+
...(current !== undefined ? { current } : {}),
|
|
210
|
+
duplicates: 0,
|
|
211
|
+
inputs: { reads: r.hashed, duplicateIds: [] },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export async function takeSnapshot(ctx, record, parsed, duplicateFields, signal) {
|
|
215
|
+
if (writeShape(record) === 'call')
|
|
216
|
+
return takeCallSnapshot(ctx, record, parsed, signal);
|
|
217
|
+
ctx.ops.freshen(ctx.client);
|
|
218
|
+
const targetKey = parsed.targets.id === undefined ? undefined : `${record.resource}:${getIds(ctx, record.resource, parsed.targets.parentId, parsed.targets.id).join('/')}`;
|
|
219
|
+
const r = createReader(ctx, targetKey, signal);
|
|
220
|
+
const { get, idsFor, anchor, reads, hashed, unresolved, companies } = r;
|
|
119
221
|
const { targets, data } = parsed;
|
|
120
222
|
let current;
|
|
121
223
|
if (targets.id !== undefined) {
|
package/dist/write/evaluate.js
CHANGED
|
@@ -18,8 +18,12 @@ export function evaluate(loaded, facts, runScope, counters) {
|
|
|
18
18
|
return deny('OP_NOT_ALLOWED', `AUTOTASK_WRITE denies ${facts.op}`, hard.hazards);
|
|
19
19
|
if (runScope === undefined)
|
|
20
20
|
return deny('TARGET_NOT_IN_SCOPE', 'this principal has no run scope', hard.hazards);
|
|
21
|
-
if (!inScope(facts, runScope))
|
|
22
|
-
|
|
21
|
+
if (!inScope(facts, runScope)) {
|
|
22
|
+
const detail = facts.companyIds.length === 0
|
|
23
|
+
? 'this write names no record whose company could be read, so it may lie outside the write scope of this run (a run scope of {"all":true} admits it)'
|
|
24
|
+
: "the target's company (read from the current record) is outside the write scope of this run";
|
|
25
|
+
return deny('TARGET_NOT_IN_SCOPE', detail, hard.hazards);
|
|
26
|
+
}
|
|
23
27
|
if (facts.blocked !== undefined)
|
|
24
28
|
return deny('MOVE_UNRESOLVED', facts.blocked, hard.hazards);
|
|
25
29
|
if (facts.duplicates > 0)
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* - `destructive` and account-wide operations (the tenant configuration resources in
|
|
9
9
|
* {@link ACCOUNT_WIDE_RESOURCES}) carry their hazard in the preview; AUTOTASK_WRITE decides their mode.
|
|
10
10
|
*
|
|
11
|
-
* The preview/apply path
|
|
12
|
-
*
|
|
13
|
-
* (the SDK's composite helpers
|
|
11
|
+
* The preview/apply path serves every write. A record whose positional arguments are one of the six
|
|
12
|
+
* CRUD shapes or the move shape gets a field-level snapshot (the target record, the parent, the data
|
|
13
|
+
* object); every other record (the SDK's composite helpers: `tickets.changeStatus`,
|
|
14
|
+
* `time_entries.approve`, the `*createIfNotExists` family, ...) takes the generic `call` shape, which
|
|
15
|
+
* echoes the caller's arguments and sends exactly them. {@link writeShape} never returns undefined
|
|
16
|
+
* for a non-read record, so AUTOTASK_WRITE can name every write.
|
|
14
17
|
*/
|
|
15
18
|
import type { CapabilityRecord } from 'node-autotask/capabilities';
|
|
16
19
|
/**
|
|
@@ -29,13 +32,22 @@ export interface HardRule {
|
|
|
29
32
|
}
|
|
30
33
|
/** The hard rule for one registry record. */
|
|
31
34
|
export declare function hardRuleFor(operation: string, record: CapabilityRecord | undefined): HardRule;
|
|
32
|
-
/**
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The argument shapes the preview/apply path knows: the positional names, in order.
|
|
37
|
+
* `call` is the generic fallback for every other argument signature (the SDK's composite helpers):
|
|
38
|
+
* it has no field-level snapshot, so it previews the arguments and impact instead.
|
|
39
|
+
*/
|
|
40
|
+
export type WriteShape = 'create' | 'create_child' | 'update' | 'update_child' | 'delete' | 'delete_child' | 'move' | 'call';
|
|
34
41
|
/**
|
|
35
42
|
* The only operations that may take the `move` shape (issue #107): the SDK's create-verify-delete move
|
|
36
|
-
* engine. An allowlist, not the signature alone: a
|
|
37
|
-
*
|
|
43
|
+
* engine. An allowlist, not the signature alone: a future SDK op that happened to share the signature
|
|
44
|
+
* must not be driven as a move (it takes the generic `call` shape instead, which never snapshots or
|
|
45
|
+
* recovers a source record).
|
|
38
46
|
*/
|
|
39
47
|
export declare const MOVE_OPS: readonly string[];
|
|
40
|
-
/**
|
|
41
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The write shape of a record. Every non-read record has one: the six CRUD shapes, the move shape for
|
|
50
|
+
* {@link MOVE_OPS}, and `call` for every other signature (so no write silently drops out of
|
|
51
|
+
* AUTOTASK_WRITE, issue #191).
|
|
52
|
+
*/
|
|
53
|
+
export declare function writeShape(record: CapabilityRecord): WriteShape;
|
package/dist/write/hard-rules.js
CHANGED
|
@@ -57,8 +57,9 @@ export function hardRuleFor(operation, record) {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* The only operations that may take the `move` shape (issue #107): the SDK's create-verify-delete move
|
|
60
|
-
* engine. An allowlist, not the signature alone: a
|
|
61
|
-
*
|
|
60
|
+
* engine. An allowlist, not the signature alone: a future SDK op that happened to share the signature
|
|
61
|
+
* must not be driven as a move (it takes the generic `call` shape instead, which never snapshots or
|
|
62
|
+
* recovers a source record).
|
|
62
63
|
*/
|
|
63
64
|
export const MOVE_OPS = ['time_entries.moveToTicket', 'ticket_charges.moveToTicket'];
|
|
64
65
|
const SHAPES = {
|
|
@@ -70,9 +71,13 @@ const SHAPES = {
|
|
|
70
71
|
'delete:parentId,id': 'delete_child',
|
|
71
72
|
'moveToTicket:sourceId,toTicketId': 'move',
|
|
72
73
|
};
|
|
73
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The write shape of a record. Every non-read record has one: the six CRUD shapes, the move shape for
|
|
76
|
+
* {@link MOVE_OPS}, and `call` for every other signature (so no write silently drops out of
|
|
77
|
+
* AUTOTASK_WRITE, issue #191).
|
|
78
|
+
*/
|
|
74
79
|
export function writeShape(record) {
|
|
75
80
|
const shape = SHAPES[`${record.operation}:${Object.keys(record.inputSchema).join(',')}`];
|
|
76
|
-
return shape === 'move' && !MOVE_OPS.includes(record.id) ?
|
|
81
|
+
return shape === 'move' && !MOVE_OPS.includes(record.id) ? 'call' : (shape ?? 'call');
|
|
77
82
|
}
|
|
78
83
|
//# sourceMappingURL=hard-rules.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iit-mcp-autotask",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "MCP server for the Autotask (Datto) PSA domain, built on the node-autotask SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Soukhomlinov",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/server": "^2.2.0",
|
|
56
|
-
"node-autotask": "0.
|
|
56
|
+
"node-autotask": "0.30.0",
|
|
57
57
|
"zod": "^4.6.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iit-mcp-autotask",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "MCP server for the Autotask (Datto) PSA API, powered by the node-autotask SDK",
|
|
5
5
|
"mcpProtocolBaseline": "2026-07-28",
|
|
6
6
|
"legacyProtocolFallback": "2025-11-25",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"capabilitiesManifest": "capabilities.json",
|
|
62
62
|
"sdk": {
|
|
63
63
|
"package": "node-autotask",
|
|
64
|
-
"versionRange": "0.
|
|
64
|
+
"versionRange": "0.30.0"
|
|
65
65
|
},
|
|
66
66
|
"vendorExtensions": false,
|
|
67
67
|
"credentialModelNote": "per-request; server-held under stdio or, on HTTP, only behind AUTOTASK_HTTP_AUTH=tokens; operator plans and the ledger are keyed by principal and tenant id, never by credential, and an apply uses the credential on the apply request"
|