@amityco/social-plus-vise 0.4.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/LICENSE +51 -0
- package/README.md +92 -0
- package/dist/outcomes.js +574 -0
- package/dist/server.js +810 -0
- package/dist/tools/compliance.js +965 -0
- package/dist/tools/docs.js +312 -0
- package/dist/tools/harness.js +229 -0
- package/dist/tools/integration.js +332 -0
- package/dist/tools/patch.js +67 -0
- package/dist/tools/project.js +908 -0
- package/dist/tools/resolve.js +120 -0
- package/dist/tools/sensors.js +185 -0
- package/dist/types.js +31 -0
- package/dist/version.js +19 -0
- package/package.json +64 -0
- package/rules/design.yaml +66 -0
- package/rules/feed.yaml +126 -0
- package/rules/live-data.yaml +66 -0
- package/rules/push.yaml +95 -0
- package/rules/sdk-lifecycle.yaml +422 -0
- package/rules/security.yaml +162 -0
- package/skills/social-plus-vise/SKILL.md +199 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, classifyOutcome, getOutcomeDefinition, hasAnswer, planContextFor, } from "../outcomes.js";
|
|
4
|
+
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
5
|
+
import { applicableComplianceRuleSummaries } from "./compliance.js";
|
|
6
|
+
import { detectCommandSensors } from "./harness.js";
|
|
7
|
+
import { inspectProject } from "./project.js";
|
|
8
|
+
export const planIntegrationTool = {
|
|
9
|
+
name: "plan_integration",
|
|
10
|
+
description: "Create a grounded, evidence-backed implementation packet before an AI coding agent edits a customer project.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
repoPath: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Absolute or relative path to the customer repository root.",
|
|
17
|
+
},
|
|
18
|
+
request: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Natural-language integration request.",
|
|
21
|
+
},
|
|
22
|
+
surfacePath: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Optional app/workspace path inside repoPath, such as apps/web or apps/mobile.",
|
|
25
|
+
},
|
|
26
|
+
answers: {
|
|
27
|
+
type: "object",
|
|
28
|
+
description: "Map of intake-question id to the customer's answer. Answered questions are removed from intake and matching stop conditions are suppressed so the host agent can re-plan after the user replies.",
|
|
29
|
+
additionalProperties: { type: "string" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["repoPath", "request"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
async call(input) {
|
|
36
|
+
const args = objectInput(input);
|
|
37
|
+
const repoPath = stringField(args, "repoPath");
|
|
38
|
+
const request = stringField(args, "request");
|
|
39
|
+
return textResult(await buildIntegrationPlan(repoPath, request, optionalStringField(args, "surfacePath"), answersFromInput(args.answers)));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
function answersFromInput(raw) {
|
|
43
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const answers = {};
|
|
47
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
48
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
49
|
+
answers[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return answers;
|
|
53
|
+
}
|
|
54
|
+
async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}) {
|
|
55
|
+
const repoRoot = path.resolve(repoPath);
|
|
56
|
+
const inspection = await inspectProject(repoRoot, surfacePath);
|
|
57
|
+
const root = inspection.effectiveRoot;
|
|
58
|
+
const outcome = classifyOutcome(request);
|
|
59
|
+
const platform = preferredPlatform(inspection.platforms);
|
|
60
|
+
const supportLevel = supportFor(outcome, platform);
|
|
61
|
+
const sensors = await detectCommandSensors(root, inspection.platforms);
|
|
62
|
+
const ctx = planContextFor({
|
|
63
|
+
request,
|
|
64
|
+
outcome,
|
|
65
|
+
platform,
|
|
66
|
+
platforms: inspection.platforms,
|
|
67
|
+
designSignals: inspection.designSignals,
|
|
68
|
+
answers,
|
|
69
|
+
});
|
|
70
|
+
const definition = getOutcomeDefinition(outcome);
|
|
71
|
+
return {
|
|
72
|
+
outcome,
|
|
73
|
+
platform,
|
|
74
|
+
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
75
|
+
availableSurfaces: inspection.surfaces,
|
|
76
|
+
supportLevel,
|
|
77
|
+
intent: intentFor(request, definition.interpretation),
|
|
78
|
+
intake: intakeFor(ctx, definition.intakeQuestions(ctx)),
|
|
79
|
+
docs: definition.docs(platform).filter((doc) => doc.path !== "unknown"),
|
|
80
|
+
requiredInputs: composeRequiredInputs(ctx, definition.requiredInputs(ctx)),
|
|
81
|
+
targetFiles: await targetFilesFor(root, outcome, platform, inspection.designSignals),
|
|
82
|
+
implementationRules: composeImplementationRules(ctx, definition.implementationRules(ctx)),
|
|
83
|
+
implementationSteps: definition.implementationSteps(ctx),
|
|
84
|
+
validation: ["validate_setup", "run_sensors", ...definition.validation(platform)],
|
|
85
|
+
applicableRules: await applicableComplianceRuleSummaries(outcome, inspection.platforms),
|
|
86
|
+
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
87
|
+
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
|
|
88
|
+
evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function intentFor(request, interpretation) {
|
|
92
|
+
const broadSocialRequest = BROAD_SOCIAL_REGEX.test(request);
|
|
93
|
+
const designRequest = DESIGN_REGEX.test(request);
|
|
94
|
+
return {
|
|
95
|
+
rawRequest: request,
|
|
96
|
+
interpretation,
|
|
97
|
+
ambiguity: broadSocialRequest || designRequest ? "high" : "medium",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function intakeFor(ctx, outcomeQuestions) {
|
|
101
|
+
const questions = [...outcomeQuestions];
|
|
102
|
+
if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
|
|
103
|
+
questions.push({
|
|
104
|
+
id: "design_source",
|
|
105
|
+
question: "Where are the app's design tokens, theme, or reusable UI components defined?",
|
|
106
|
+
why: "The user asked to match the existing design, and no local design source was detected.",
|
|
107
|
+
required: true,
|
|
108
|
+
blocksImplementationWhenMissing: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (ctx.mentionsDesign && ctx.designSignals.length > 0 && !hasAnswer(ctx.answers, "confirm_design_source")) {
|
|
112
|
+
questions.push({
|
|
113
|
+
id: "confirm_design_source",
|
|
114
|
+
question: `Should the social UI use the detected design source(s): ${ctx.designSignals.map((signal) => signal.file).join(", ")}?`,
|
|
115
|
+
why: "Vise found likely design evidence, but the user or host agent should confirm it is the right source before UI edits.",
|
|
116
|
+
required: true,
|
|
117
|
+
blocksImplementationWhenMissing: false,
|
|
118
|
+
options: ["yes", "use another source"],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
|
|
122
|
+
return {
|
|
123
|
+
status: remainingBlocking > 0 ? "needs-clarification" : "ready",
|
|
124
|
+
questions,
|
|
125
|
+
answers: ctx.answers,
|
|
126
|
+
remainingBlocking,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function composeRequiredInputs(ctx, outcomeInputs) {
|
|
130
|
+
const inputs = [
|
|
131
|
+
"social.plus API key local env/config variable name and placeholder; do not collect the secret value in chat",
|
|
132
|
+
"social.plus region: US, EU, or SG",
|
|
133
|
+
"user identity source for login",
|
|
134
|
+
...outcomeInputs,
|
|
135
|
+
];
|
|
136
|
+
if ((ctx.mentionsDesign || ctx.broadSocialRequest) && ctx.designSignals.length > 0) {
|
|
137
|
+
inputs.push(`confirm detected design source(s): ${ctx.designSignals.map((signal) => signal.file).join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
if ((ctx.mentionsDesign || ctx.broadSocialRequest) && ctx.designSignals.length === 0) {
|
|
140
|
+
inputs.push("design token or theme source file", "UI framework/design system used by the host app");
|
|
141
|
+
}
|
|
142
|
+
if (ctx.platform === "android") {
|
|
143
|
+
inputs.push("Android application module path");
|
|
144
|
+
}
|
|
145
|
+
if (ctx.platform === "ios") {
|
|
146
|
+
inputs.push("iOS dependency manager: Swift Package Manager or CocoaPods");
|
|
147
|
+
}
|
|
148
|
+
return inputs;
|
|
149
|
+
}
|
|
150
|
+
function composeImplementationRules(ctx, outcomeRules) {
|
|
151
|
+
const rules = [
|
|
152
|
+
"Do not ask the user to paste API keys, Firebase/APNS credentials, server-issued auth tokens, or other secrets into chat.",
|
|
153
|
+
"Do not invent API keys, app IDs, regions, user IDs, Firebase files, APNS keys, or server-issued auth tokens.",
|
|
154
|
+
"Create or update an env/config placeholder for required secrets and tell the user to fill the real value locally; only write local env files when they are ignored by git.",
|
|
155
|
+
"Do not silently default the social.plus region; ask for US, EU, or SG if it is not known.",
|
|
156
|
+
"Do not make SDK calls before setup and login ordering is established.",
|
|
157
|
+
"Every code edit should be tied to a target file and validation step from this plan.",
|
|
158
|
+
];
|
|
159
|
+
if (ctx.platform === "android") {
|
|
160
|
+
rules.push("Do not initialize the SDK from an Activity, Fragment, or composable lifecycle when Application startup is available.");
|
|
161
|
+
}
|
|
162
|
+
if (ctx.platform === "flutter") {
|
|
163
|
+
rules.push("Call WidgetsFlutterBinding.ensureInitialized before async SDK setup in main().");
|
|
164
|
+
}
|
|
165
|
+
if (ctx.platform === "react-native") {
|
|
166
|
+
rules.push("Do not create the client inside a remounting React component body.");
|
|
167
|
+
}
|
|
168
|
+
rules.push(...outcomeRules);
|
|
169
|
+
if (ctx.mentionsDesign) {
|
|
170
|
+
rules.push("Do not create a new visual system. Inspect and reuse the host app's existing design tokens, theme files, components, spacing, typography, and color conventions.");
|
|
171
|
+
}
|
|
172
|
+
return rules;
|
|
173
|
+
}
|
|
174
|
+
function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath) {
|
|
175
|
+
const stops = [
|
|
176
|
+
"A required secret is missing and no safe ignored local env file or non-secret template path is clear.",
|
|
177
|
+
"The target file is ambiguous or missing and no safe conventional location is detected.",
|
|
178
|
+
"Docs lookup does not return the canonical page named in this plan.",
|
|
179
|
+
];
|
|
180
|
+
if (ctx.platform === "unknown" || ctx.outcome === "unknown") {
|
|
181
|
+
stops.push("The request or platform is unsupported; do not implement until clarified.");
|
|
182
|
+
}
|
|
183
|
+
if (ctx.platforms.length > 1 && !surfacePath) {
|
|
184
|
+
stops.push(`Multiple platform signals detected (${ctx.platforms.join(", ")}); confirm which app surface should be modified.`);
|
|
185
|
+
}
|
|
186
|
+
if (surfaces.length > 1 && !surfacePath) {
|
|
187
|
+
stops.push(`Multiple app surfaces detected (${surfaces.map((surface) => surface.path).join(", ")}); call this tool again with surfacePath set to the target app surface.`);
|
|
188
|
+
}
|
|
189
|
+
stops.push(...outcomeStops);
|
|
190
|
+
if (ctx.broadSocialRequest && !hasAnswer(ctx.answers, "feature_surface")) {
|
|
191
|
+
stops.push("The requested social feature is too broad; confirm the first feature surface before implementing.");
|
|
192
|
+
}
|
|
193
|
+
if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
|
|
194
|
+
stops.push("No design token, theme, or component source has been identified from the customer repo.");
|
|
195
|
+
}
|
|
196
|
+
return stops;
|
|
197
|
+
}
|
|
198
|
+
function preferredPlatform(platforms) {
|
|
199
|
+
const order = ["flutter", "android", "typescript", "react-native", "ios"];
|
|
200
|
+
return order.find((platform) => platforms.includes(platform)) ?? platforms[0] ?? "unknown";
|
|
201
|
+
}
|
|
202
|
+
function supportFor(outcome, platform) {
|
|
203
|
+
if (outcome === "unknown" || platform === "unknown") {
|
|
204
|
+
return "unsupported";
|
|
205
|
+
}
|
|
206
|
+
if (platform === "ios") {
|
|
207
|
+
return "guided";
|
|
208
|
+
}
|
|
209
|
+
if (["android", "flutter", "typescript", "react-native"].includes(platform)) {
|
|
210
|
+
return "supported";
|
|
211
|
+
}
|
|
212
|
+
return "guided";
|
|
213
|
+
}
|
|
214
|
+
async function targetFilesFor(root, outcome, platform, designSignals) {
|
|
215
|
+
const designTargets = outcome === "add-feed"
|
|
216
|
+
? designSignals.map((signal) => ({
|
|
217
|
+
path: signal.file,
|
|
218
|
+
operation: "Reuse existing design tokens, theme values, component conventions, spacing, typography, and colors for the social UI.",
|
|
219
|
+
confidence: "high",
|
|
220
|
+
evidence: signal.reason,
|
|
221
|
+
}))
|
|
222
|
+
: [];
|
|
223
|
+
const pushTargets = outcome === "setup-push"
|
|
224
|
+
? [
|
|
225
|
+
{
|
|
226
|
+
path: "push token/permission flow",
|
|
227
|
+
operation: "Request permission, obtain the platform device token, and handle token refresh.",
|
|
228
|
+
confidence: "low",
|
|
229
|
+
evidence: "Required device token source from the host app.",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
path: "login/auth flow",
|
|
233
|
+
operation: "Register the device token only after social.plus login succeeds.",
|
|
234
|
+
confidence: "low",
|
|
235
|
+
evidence: "Device registration requires an authenticated client.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
path: "logout/user-switch flow",
|
|
239
|
+
operation: "Unregister the device token before logout or user switch completes.",
|
|
240
|
+
confidence: "low",
|
|
241
|
+
evidence: "Device association is user-specific.",
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
: [];
|
|
245
|
+
const liveTargets = outcome === "setup-live-data"
|
|
246
|
+
? [
|
|
247
|
+
{
|
|
248
|
+
path: "target screen/component/controller",
|
|
249
|
+
operation: "Observe the Live Object or Live Collection and render loading, empty, error, and data states.",
|
|
250
|
+
confidence: "low",
|
|
251
|
+
evidence: "Required lifecycle owner and target UI.",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: "data source/repository module",
|
|
255
|
+
operation: "Encapsulate query/get calls and expose a cleanup-aware subscription API.",
|
|
256
|
+
confidence: "low",
|
|
257
|
+
evidence: "Live Objects and Collections require lifecycle cleanup.",
|
|
258
|
+
},
|
|
259
|
+
]
|
|
260
|
+
: [];
|
|
261
|
+
if (platform === "android") {
|
|
262
|
+
return dedupeTargetsByPath([
|
|
263
|
+
await target(root, ["app/build.gradle.kts", "app/build.gradle"], "Add or verify the social.plus Android SDK dependency.", "Detected Android app Gradle module.", "medium"),
|
|
264
|
+
await target(root, ["app/src/main/AndroidManifest.xml"], "Verify permissions and Application class wiring.", "Detected default Android manifest path.", "medium"),
|
|
265
|
+
{ path: "Application class", operation: "Initialize social.plus once at app startup.", confidence: "low", evidence: "Required by Android setup lifecycle rule; exact file must be detected by the coding agent." },
|
|
266
|
+
{ path: "login/auth flow", operation: "Call social.plus login after user identity is known.", confidence: "low", evidence: "Required user identity source." },
|
|
267
|
+
...pushTargets,
|
|
268
|
+
...liveTargets,
|
|
269
|
+
...designTargets,
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
if (platform === "flutter") {
|
|
273
|
+
return dedupeTargetsByPath([
|
|
274
|
+
await target(root, ["pubspec.yaml"], "Add or verify the Flutter SDK dependency.", "Detected Flutter package manifest.", "high"),
|
|
275
|
+
await target(root, ["lib/main.dart"], "Initialize Flutter binding and SDK before runApp.", "Conventional Flutter app entrypoint.", "medium"),
|
|
276
|
+
{ path: "login/auth flow", operation: "Call AmityCoreClient.login after user identity is known.", confidence: "low", evidence: "Required user identity source." },
|
|
277
|
+
...pushTargets,
|
|
278
|
+
...liveTargets,
|
|
279
|
+
...designTargets,
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
if (platform === "typescript" || platform === "react-native") {
|
|
283
|
+
return dedupeTargetsByPath([
|
|
284
|
+
await target(root, ["package.json"], "Add or verify the TypeScript SDK dependency.", "Detected JavaScript package manifest.", "high"),
|
|
285
|
+
{ path: "client initialization module", operation: "Create the social.plus client with API key and region.", confidence: "low", evidence: "Validator rule: typescript.client.create." },
|
|
286
|
+
{ path: "login/auth flow", operation: "Await login after user identity is known and before live subscriptions.", confidence: "low", evidence: "Required user identity source." },
|
|
287
|
+
...pushTargets,
|
|
288
|
+
...liveTargets,
|
|
289
|
+
...designTargets,
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
if (platform === "ios") {
|
|
293
|
+
return dedupeTargetsByPath([
|
|
294
|
+
await target(root, ["Package.swift", "Podfile"], "Add or verify the iOS SDK dependency.", "Detected iOS dependency manifest if present.", "medium"),
|
|
295
|
+
{ path: "app initialization flow", operation: "Initialize AmityClient with API key and region.", confidence: "low", evidence: "iOS quick-start docs." },
|
|
296
|
+
{ path: "login/auth flow", operation: "Call login after user identity is known and handle session renewal.", confidence: "low", evidence: "Authentication docs." },
|
|
297
|
+
...pushTargets,
|
|
298
|
+
...liveTargets,
|
|
299
|
+
...designTargets,
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
302
|
+
return [{ path: "unknown", operation: "No safe target files can be selected until platform is detected.", confidence: "low", evidence: "No platform signal." }];
|
|
303
|
+
}
|
|
304
|
+
function dedupeTargetsByPath(targets) {
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
const out = [];
|
|
307
|
+
for (const target of targets) {
|
|
308
|
+
if (seen.has(target.path)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
seen.add(target.path);
|
|
312
|
+
out.push(target);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
async function target(root, candidates, operation, evidence, fallbackConfidence) {
|
|
317
|
+
for (const candidate of candidates) {
|
|
318
|
+
if (await exists(path.join(root, candidate))) {
|
|
319
|
+
return { path: candidate, operation, confidence: "high", evidence };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { path: candidates[0], operation, confidence: fallbackConfidence, evidence: `${evidence} File was not found at the conventional path; verify before editing.` };
|
|
323
|
+
}
|
|
324
|
+
async function exists(filePath) {
|
|
325
|
+
try {
|
|
326
|
+
await access(filePath);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { objectInput, stringField, textResult } from "../types.js";
|
|
2
|
+
export const generateGuardedPatchTool = {
|
|
3
|
+
name: "generate_guarded_patch",
|
|
4
|
+
description: "Yields a structured diff for the host AI agent to apply safely, rejecting any inline API keys or hardcoded IDs.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
repoPath: { type: "string" },
|
|
9
|
+
targetFile: { type: "string", description: "Relative path to the file." },
|
|
10
|
+
diffContent: { type: "string", description: "The proposed code change." },
|
|
11
|
+
},
|
|
12
|
+
required: ["repoPath", "targetFile", "diffContent"],
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
},
|
|
15
|
+
async call(input) {
|
|
16
|
+
const args = objectInput(input);
|
|
17
|
+
const repoPath = stringField(args, "repoPath");
|
|
18
|
+
const targetFile = stringField(args, "targetFile");
|
|
19
|
+
const diffContent = stringField(args, "diffContent");
|
|
20
|
+
// Guardrail: Inline API Keys
|
|
21
|
+
const inlineApiKeyPattern = /\b(api[_-]?key)\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i;
|
|
22
|
+
const apiKeyMatch = inlineApiKeyPattern.exec(diffContent);
|
|
23
|
+
if (apiKeyMatch && !isAllowedPlaceholder(apiKeyMatch[2] ?? "")) {
|
|
24
|
+
return textResult({
|
|
25
|
+
status: "rejected",
|
|
26
|
+
reason: "The patch contains an inline API key. Please use environment variables or a configuration placeholder instead.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Guardrail: Hardcoded IDs
|
|
30
|
+
const hardcodedIdPattern = /\b(communityId|targetId|feedId|channelId)\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i;
|
|
31
|
+
const idMatch = hardcodedIdPattern.exec(diffContent);
|
|
32
|
+
if (idMatch && !isAllowedPlaceholder(idMatch[2] ?? "")) {
|
|
33
|
+
return textResult({
|
|
34
|
+
status: "rejected",
|
|
35
|
+
reason: `The patch contains a hardcoded literal for ${idMatch[1]}. Please do not invent or hardcode feed targets.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return textResult({
|
|
39
|
+
status: "approved",
|
|
40
|
+
repoPath,
|
|
41
|
+
targetFile,
|
|
42
|
+
diffContent,
|
|
43
|
+
instruction: "Host agent: This patch has passed Foundry guardrails. You may now apply this diff securely.",
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function isAllowedPlaceholder(value) {
|
|
48
|
+
const normalized = value.toLowerCase().trim();
|
|
49
|
+
return [
|
|
50
|
+
"",
|
|
51
|
+
"api-key",
|
|
52
|
+
"your-api-key",
|
|
53
|
+
"your_api_key",
|
|
54
|
+
"provided-by-customer",
|
|
55
|
+
"customer-provided",
|
|
56
|
+
"replace-me",
|
|
57
|
+
"todo",
|
|
58
|
+
"community-id",
|
|
59
|
+
"community_id",
|
|
60
|
+
"target-id",
|
|
61
|
+
"target_id",
|
|
62
|
+
"feed-id",
|
|
63
|
+
"feed_id",
|
|
64
|
+
"channel-id",
|
|
65
|
+
"channel_id",
|
|
66
|
+
].includes(normalized);
|
|
67
|
+
}
|