fullstackgtm 0.48.0 → 0.49.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/CHANGELOG.md +38 -0
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +7 -2
- package/INSTALL_FOR_AGENTS.md +13 -4
- package/README.md +12 -1
- package/SECURITY.md +27 -3
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/fix.js +20 -7
- package/dist/cli/help.js +10 -6
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +56 -5
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/prospectSources.js +4 -11
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/salesforceAuth.d.ts +9 -0
- package/dist/connectors/salesforceAuth.js +47 -5
- package/dist/connectors/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/market.d.ts +1 -1
- package/dist/market.js +7 -127
- package/dist/marketClassify.d.ts +10 -0
- package/dist/marketClassify.js +52 -1
- package/dist/marketSourcing.js +7 -45
- package/dist/mcp.js +67 -115
- package/dist/planStore.d.ts +28 -1
- package/dist/planStore.js +195 -49
- package/dist/providerError.d.ts +21 -0
- package/dist/providerError.js +30 -0
- package/dist/publicHttp.d.ts +28 -0
- package/dist/publicHttp.js +143 -0
- package/dist/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +1 -1
- package/docs/api.md +29 -2
- package/docs/architecture.md +13 -2
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +7 -1
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/fix.ts +28 -7
- package/src/cli/help.ts +10 -6
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +66 -5
- package/src/cli/ui.ts +1 -0
- package/src/cli.ts +1 -1
- package/src/config.ts +39 -1
- package/src/connectors/prospectSources.ts +4 -11
- package/src/connectors/salesforce.ts +12 -5
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/index.ts +4 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +93 -136
- package/src/planStore.ts +235 -57
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/secureFile.ts +169 -0
- package/src/types.ts +1 -0
package/src/marketSourcing.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Sitemaps / redirects / logo bytes are plain resources — they never need a
|
|
17
17
|
* headless browser, so the default fetch is sufficient even in the hosted layer.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { publicHttpGet } from "./publicHttp.ts";
|
|
20
20
|
import { DEFAULT_MODELS, forcedToolCall, type LlmCallOptions } from "./llm.ts";
|
|
21
21
|
|
|
22
22
|
const USER_AGENT = "fullstackgtm-market/0 (+https://github.com/fullstackgtm/core)";
|
|
@@ -161,27 +161,9 @@ export function extractLogoUrl(html: string, baseUrl: string): string | null {
|
|
|
161
161
|
|
|
162
162
|
/** Manual-redirect fetch that re-validates every hop against the SSRF guard. */
|
|
163
163
|
async function guardedFetch(url: string): Promise<Response | null> {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const res = await fetch(current, {
|
|
168
|
-
redirect: "manual",
|
|
169
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
170
|
-
headers: { "User-Agent": USER_AGENT },
|
|
171
|
-
});
|
|
172
|
-
const location = res.headers.get("location");
|
|
173
|
-
if (res.status >= 300 && res.status < 400 && location) {
|
|
174
|
-
try {
|
|
175
|
-
await res.body?.cancel();
|
|
176
|
-
} catch {
|
|
177
|
-
/* ignore */
|
|
178
|
-
}
|
|
179
|
-
current = new URL(location, current).toString();
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
return res;
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
164
|
+
const res = await publicHttpGet(url, { maxRedirects: MAX_REDIRECTS, timeoutMs: FETCH_TIMEOUT_MS,
|
|
165
|
+
headers: { "User-Agent": USER_AGENT } });
|
|
166
|
+
return new Response(res.body.length ? Buffer.from(res.body) : null, { status: res.status, headers: res.headers });
|
|
185
167
|
}
|
|
186
168
|
|
|
187
169
|
const defaultFetchText: FetchText = async (url) => {
|
|
@@ -210,27 +192,9 @@ const defaultFetchBytes: FetchBytes = async (url) => {
|
|
|
210
192
|
* + status WITHOUT downloading the body. Used to detect identity drift.
|
|
211
193
|
*/
|
|
212
194
|
export const resolveFinalUrl: ResolveUrl = async (rawUrl) => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const res = await fetch(current, {
|
|
217
|
-
redirect: "manual",
|
|
218
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
219
|
-
headers: { "User-Agent": USER_AGENT },
|
|
220
|
-
});
|
|
221
|
-
try {
|
|
222
|
-
await res.body?.cancel();
|
|
223
|
-
} catch {
|
|
224
|
-
/* ignore */
|
|
225
|
-
}
|
|
226
|
-
const location = res.headers.get("location");
|
|
227
|
-
if (res.status >= 300 && res.status < 400 && location) {
|
|
228
|
-
current = new URL(location, current).toString();
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
return { finalUrl: current, status: res.status };
|
|
232
|
-
}
|
|
233
|
-
return { finalUrl: current, status: 0 };
|
|
195
|
+
const res = await publicHttpGet(rawUrl, { maxRedirects: MAX_REDIRECTS, timeoutMs: FETCH_TIMEOUT_MS,
|
|
196
|
+
maxBytes: 0, headers: { "User-Agent": USER_AGENT } });
|
|
197
|
+
return { finalUrl: res.finalUrl, status: res.status };
|
|
234
198
|
};
|
|
235
199
|
|
|
236
200
|
// ── Fetching helpers ─────────────────────────────────────────────────────────
|
package/src/mcp.ts
CHANGED
|
@@ -55,11 +55,12 @@ import { generateDemoSnapshot } from "./demo.ts";
|
|
|
55
55
|
import type { FieldMappings } from "./mappings.ts";
|
|
56
56
|
import { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
57
57
|
import { verifyApprovalDigests } from "./integrity.ts";
|
|
58
|
-
import { createFilePlanStore
|
|
58
|
+
import { createFilePlanStore } from "./planStore.ts";
|
|
59
59
|
import { builtinAuditRules } from "./rules.ts";
|
|
60
60
|
import { sampleSnapshot } from "./sampleData.ts";
|
|
61
61
|
import { normalizeTranscript, parseCall } from "./calls.ts";
|
|
62
62
|
import { extractInsightsLlm, resolveLlmCredential } from "./llm.ts";
|
|
63
|
+
|
|
63
64
|
import {
|
|
64
65
|
computeFrontStates,
|
|
65
66
|
createFileObservationStore,
|
|
@@ -204,10 +205,19 @@ const toolDefinitions: ToolDefinition[] = [
|
|
|
204
205
|
},
|
|
205
206
|
handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
|
|
206
207
|
const loaded = configPath ? loadConfig(configPath) : null;
|
|
208
|
+
if (loaded?.config.rulePackages?.length) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"MCP refuses executable rulePackages from tool-call configPath. " +
|
|
211
|
+
"MCP plugin execution is disabled because mutable tool-call paths are not a durable code-trust boundary. " +
|
|
212
|
+
"Run reviewed plugins through the CLI with --config <path> --allow-plugins instead.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
207
215
|
const policy = mergePolicy(defaultPolicy(today), loaded?.config);
|
|
208
216
|
if (today) policy.today = today;
|
|
209
217
|
if (staleDealDays !== undefined) policy.staleDealDays = staleDealDays;
|
|
210
|
-
const ruleSet = await resolveConfiguredRules(loaded
|
|
218
|
+
const ruleSet = await resolveConfiguredRules(loaded, undefined, {
|
|
219
|
+
disableRulePackages: true,
|
|
220
|
+
});
|
|
211
221
|
const selected = rules?.length
|
|
212
222
|
? ruleSet.filter((rule: { id: string }) => rules.includes(rule.id))
|
|
213
223
|
: ruleSet;
|
|
@@ -327,143 +337,104 @@ const toolDefinitions: ToolDefinition[] = [
|
|
|
327
337
|
config: {
|
|
328
338
|
title: "Apply Approved Patch Operations",
|
|
329
339
|
description:
|
|
330
|
-
"Apply
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"the store's HMAC approval digests — ids not approved with `plans approve` are " +
|
|
335
|
-
"refused — and the run is recorded onto the stored plan so `plans show` and " +
|
|
340
|
+
"Apply a stored, human-approved patch plan through a provider connector. The plan, " +
|
|
341
|
+
"approved operation ids, and concrete placeholder values are loaded exclusively from " +
|
|
342
|
+
"the local plan store and verified against its HMAC approval digests. Callers cannot " +
|
|
343
|
+
"supply or widen approvals. Every apply run is recorded so `plans show` and " +
|
|
336
344
|
"`audit-log export` include it.",
|
|
337
345
|
inputSchema: strictSchema({
|
|
338
346
|
provider: z.enum(["hubspot", "salesforce"]),
|
|
339
|
-
|
|
340
|
-
approvedOperationIds: z.array(z.string()).min(1),
|
|
341
|
-
valueOverrides: z.record(z.string(), z.string()).optional(),
|
|
347
|
+
planId: z.string().regex(/^[\w.-]+$/),
|
|
342
348
|
output: z.enum(["json", "markdown"]).optional(),
|
|
343
349
|
}),
|
|
344
350
|
},
|
|
345
|
-
handler: async ({ provider,
|
|
346
|
-
// The
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (!filePlan || !Array.isArray(filePlan.operations)) {
|
|
351
|
-
throw new Error(`${planPath} is not a patch plan (expected { id, operations: [...] }).`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Governance parity with CLI apply: when this plan id exists in the local
|
|
355
|
-
// plan store, the STORE is the source of truth — the approved-id set must
|
|
356
|
-
// come from `plans approve` (which HMAC-signed each approved op), the
|
|
357
|
-
// signatures are re-verified here, and the run is recorded back onto the
|
|
358
|
-
// plan so audit-log export sees MCP applies exactly like CLI applies.
|
|
351
|
+
handler: async ({ provider, planId, output }) => {
|
|
352
|
+
// The store is the only authority accepted over MCP. In particular, do
|
|
353
|
+
// not accept a plan body/path, approval ids, or value overrides from the
|
|
354
|
+
// agent call: combining proposal and approval in one call defeats the
|
|
355
|
+
// human approval boundary the plan lifecycle is meant to enforce.
|
|
359
356
|
const store = createFilePlanStore();
|
|
360
|
-
const stored =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let plan: PatchPlan;
|
|
366
|
-
let effectiveOverrides: Record<string, unknown>;
|
|
367
|
-
if (stored) {
|
|
368
|
-
if (stored.status !== "approved") {
|
|
369
|
-
throw new Error(
|
|
370
|
-
`Plan ${filePlan.id} is ${stored.status}; approve operations first with ` +
|
|
371
|
-
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids|all>\`.`,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
// Downgrade guard (same as CLI apply): an approved plan with no
|
|
375
|
-
// signatures either predates 0.26 or had approvalDigests stripped to
|
|
376
|
-
// skip the integrity check. Refuse rather than trust the file.
|
|
377
|
-
if (stored.approvedOperationIds.length > 0 && !stored.approvalDigests) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
`Refusing to apply plan ${filePlan.id}: it was approved without integrity signatures ` +
|
|
380
|
-
"(approved before 0.26.0, or its signatures were removed). Re-approve it with " +
|
|
381
|
-
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids|all>\`.`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
// Never widen: every id the tool wants applied must already be
|
|
385
|
-
// store-approved. (A subset is fine — narrowing never writes more.)
|
|
386
|
-
const storeApproved = new Set(stored.approvedOperationIds);
|
|
387
|
-
const unapproved = approvedOperationIds.filter((id: string) => !storeApproved.has(id));
|
|
388
|
-
if (unapproved.length > 0) {
|
|
389
|
-
throw new Error(
|
|
390
|
-
`Refusing to apply plan ${filePlan.id}: operation(s) ${unapproved.join(", ")} were never ` +
|
|
391
|
-
"approved in the plan store. Approve them first with " +
|
|
392
|
-
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids>\`.`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
// Integrity gate: verify against the EFFECTIVE overrides (stored ∪ tool
|
|
396
|
-
// args), so a tool-supplied value that changes what the human approved
|
|
397
|
-
// is treated as tamper, not a live override — what gets written must
|
|
398
|
-
// equal what was signed.
|
|
399
|
-
effectiveOverrides = { ...stored.valueOverrides, ...(valueOverrides ?? {}) };
|
|
400
|
-
const verification = verifyApprovalDigests(
|
|
401
|
-
stored.plan.operations,
|
|
402
|
-
stored.approvedOperationIds,
|
|
403
|
-
effectiveOverrides,
|
|
404
|
-
stored.approvalDigests,
|
|
357
|
+
const stored = await store.get(planId);
|
|
358
|
+
if (!stored) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`No stored plan with id ${planId}. Save it with \`fullstackgtm audit --save\`, then approve it with \`fullstackgtm plans approve\`.`,
|
|
405
361
|
);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
362
|
+
}
|
|
363
|
+
if (stored.status !== "approved") {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Plan ${planId} is ${stored.status}; approve operations first with ` +
|
|
366
|
+
`\`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (stored.approvedOperationIds.length === 0) {
|
|
370
|
+
throw new Error(`Plan ${planId} has no approved operations.`);
|
|
371
|
+
}
|
|
372
|
+
if (!stored.approvalDigests) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Refusing to apply plan ${planId}: it was approved without integrity signatures ` +
|
|
375
|
+
"(approved before 0.26.0, or its signatures were removed). Re-approve it with " +
|
|
376
|
+
`\`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const verification = verifyApprovalDigests(
|
|
380
|
+
stored.plan.operations,
|
|
381
|
+
stored.approvedOperationIds,
|
|
382
|
+
stored.valueOverrides,
|
|
383
|
+
stored.approvalDigests,
|
|
384
|
+
);
|
|
385
|
+
if (!verification.ok) {
|
|
386
|
+
const detail =
|
|
387
|
+
verification.reason === "no_key"
|
|
388
|
+
? "the plan-signing key is missing (was this plan approved on another machine?). Re-approve it here with `fullstackgtm plans approve`."
|
|
389
|
+
: `these operations differ from what was approved: ${verification.tampered.join(", ")}. ` +
|
|
390
|
+
"The stored plan or approved values changed after approval — review and re-approve.";
|
|
391
|
+
throw new Error(`Refusing to apply plan ${planId}: ${detail}`);
|
|
429
392
|
}
|
|
430
393
|
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
394
|
+
const connector = await connectorFor(provider);
|
|
395
|
+
// Claim only after all preflight checks and credential resolution. The
|
|
396
|
+
// atomic store transition prevents a second CLI/MCP process from
|
|
397
|
+
// applying the same approval concurrently.
|
|
398
|
+
const claimed = await store.claimApply(planId, { provider, source: "mcp" });
|
|
399
|
+
const claimedVerification = verifyApprovalDigests(
|
|
400
|
+
claimed.stored.plan.operations,
|
|
401
|
+
claimed.stored.approvedOperationIds,
|
|
402
|
+
claimed.stored.valueOverrides,
|
|
403
|
+
claimed.stored.approvalDigests,
|
|
404
|
+
);
|
|
405
|
+
if (!claimedVerification.ok) {
|
|
406
|
+
await store.abortApplyPreflight(
|
|
407
|
+
planId,
|
|
408
|
+
claimed.claimId,
|
|
409
|
+
"Approval integrity verification failed after the claim and before provider I/O.",
|
|
410
|
+
);
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Refusing to apply plan ${planId}: the claimed plan differs from what was approved: ` +
|
|
413
|
+
`${claimedVerification.tampered.join(", ") || "signing key unavailable"}.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
let run;
|
|
417
|
+
try {
|
|
418
|
+
run = await applyPatchPlan(connector, claimed.stored.plan, {
|
|
419
|
+
approvedOperationIds: claimed.stored.approvedOperationIds,
|
|
420
|
+
valueOverrides: claimed.stored.valueOverrides,
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await store.markApplyUncertain(planId, claimed.claimId);
|
|
424
|
+
throw error;
|
|
443
425
|
}
|
|
426
|
+
await store.recordRun(claimed.stored.plan.id, run, claimed.claimId);
|
|
444
427
|
const governance = {
|
|
445
|
-
planSource:
|
|
446
|
-
approvalDigestsVerified:
|
|
447
|
-
runRecorded,
|
|
448
|
-
...(stored
|
|
449
|
-
? {}
|
|
450
|
-
: {
|
|
451
|
-
note:
|
|
452
|
-
"Plan file is not in the local plan store: approvals came from tool arguments " +
|
|
453
|
-
"(verified against the plan content only — no approval-digest check) and the run " +
|
|
454
|
-
"was not recorded. Use `fullstackgtm audit --save` + `plans approve` for " +
|
|
455
|
-
"store-backed governance.",
|
|
456
|
-
}),
|
|
428
|
+
planSource: "store" as const,
|
|
429
|
+
approvalDigestsVerified: true,
|
|
430
|
+
runRecorded: true,
|
|
457
431
|
};
|
|
458
432
|
if (output === "markdown") {
|
|
459
433
|
return content(
|
|
460
434
|
`${formatPatchPlanRun(run)}\n\nGovernance: plan source ${governance.planSource}; ` +
|
|
461
|
-
(
|
|
462
|
-
? "approval digests verified; run recorded on the stored plan (audit-log export includes it)."
|
|
463
|
-
: governance.note!),
|
|
435
|
+
"approval digests verified; run recorded on the stored plan (audit-log export includes it).",
|
|
464
436
|
);
|
|
465
437
|
}
|
|
466
|
-
// Backward compatible: the run's own fields stay top-level; governance is additive.
|
|
467
438
|
return content({ ...run, governance });
|
|
468
439
|
},
|
|
469
440
|
},
|
|
@@ -551,7 +522,7 @@ toolDefinitions.unshift({
|
|
|
551
522
|
})),
|
|
552
523
|
safety:
|
|
553
524
|
"Only tools flagged writesCrm can reach a CRM; fullstackgtm_apply writes only operations " +
|
|
554
|
-
"
|
|
525
|
+
"from a stored plan whose ids and values were human-approved and HMAC-verified.",
|
|
555
526
|
server: { command: "npx -y fullstackgtm-mcp" },
|
|
556
527
|
cli: {
|
|
557
528
|
command: "npx fullstackgtm <command> [--json]",
|
|
@@ -582,20 +553,6 @@ export async function startMcpServer() {
|
|
|
582
553
|
await server.connect(transport);
|
|
583
554
|
}
|
|
584
555
|
|
|
585
|
-
/**
|
|
586
|
-
* A plan-store file is a StoredPlan envelope ({ plan, status, runs, ... });
|
|
587
|
-
* `audit --out` writes a bare PatchPlan. fullstackgtm_apply accepts either.
|
|
588
|
-
*/
|
|
589
|
-
function isStoredPlanFile(value: unknown): value is StoredPlan {
|
|
590
|
-
if (typeof value !== "object" || value === null || !("plan" in value)) return false;
|
|
591
|
-
const plan = (value as { plan?: unknown }).plan;
|
|
592
|
-
return (
|
|
593
|
-
typeof plan === "object" &&
|
|
594
|
-
plan !== null &&
|
|
595
|
-
Array.isArray((plan as { operations?: unknown }).operations)
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
556
|
function loadMarketConfigOrHint(path: string): ReturnType<typeof loadMarketConfig> {
|
|
600
557
|
try {
|
|
601
558
|
return loadMarketConfig(path);
|