@frankieai/server 0.2.3 → 0.2.5
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/dist/{chunk-MKN5663J.js → chunk-HPYT6ZIU.js} +100 -12
- package/dist/chunk-HPYT6ZIU.js.map +1 -0
- package/dist/{chunk-37VMCQCI.js → chunk-SQ5NJKFE.js} +44 -3
- package/dist/chunk-SQ5NJKFE.js.map +1 -0
- package/dist/control-server.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -2
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-37VMCQCI.js.map +0 -1
- package/dist/chunk-MKN5663J.js.map +0 -1
|
@@ -441,7 +441,6 @@ var OpenApiError = class extends Error {
|
|
|
441
441
|
};
|
|
442
442
|
async function inspectApiCapabilities(source, options = {}) {
|
|
443
443
|
const doc = await loadSpec(source.spec, options.fetchImpl);
|
|
444
|
-
const allowedMethods = new Set(source.methods);
|
|
445
444
|
const hasPerUserAuth = Boolean(source.forwardUserTokenAs) || source.forwardUserClaims.length > 0;
|
|
446
445
|
const capabilities = [];
|
|
447
446
|
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
@@ -451,7 +450,6 @@ async function inspectApiCapabilities(source, options = {}) {
|
|
|
451
450
|
const operation = item[method];
|
|
452
451
|
if (!isRecord(operation)) continue;
|
|
453
452
|
const httpMethod = method.toUpperCase();
|
|
454
|
-
if (!allowedMethods.has(httpMethod)) continue;
|
|
455
453
|
const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : `${method}_${slug(path)}`;
|
|
456
454
|
if (source.operations.length > 0 && !source.operations.includes(operationId)) continue;
|
|
457
455
|
const toolName = `${API_TOOL_PREFIX}${source.name}__${sanitize(operationId)}`;
|
|
@@ -499,14 +497,14 @@ async function buildOne(source, options) {
|
|
|
499
497
|
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
500
498
|
const operation = item[method];
|
|
501
499
|
if (!isRecord(operation)) continue;
|
|
502
|
-
if (!allowedMethods.has(method.toUpperCase())) continue;
|
|
503
500
|
const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : `${method}_${slug(path)}`;
|
|
504
501
|
if (source.operations.length > 0 && !source.operations.includes(operationId)) continue;
|
|
505
|
-
if (source.selectedTools
|
|
502
|
+
if (source.selectedTools ? !source.selectedTools.includes(operationId) : !allowedMethods.has(method.toUpperCase())) continue;
|
|
506
503
|
const name = `${API_TOOL_PREFIX}${source.name}__${sanitize(operationId)}`;
|
|
507
504
|
if (!evaluateToolPolicy(name, options.policy).allowed) continue;
|
|
508
505
|
const params = collectParameters(doc, item, operation);
|
|
509
506
|
const body = collectRequestBody(doc, operation);
|
|
507
|
+
const scopeRules = parseScopeRules(operation);
|
|
510
508
|
const { headers: securedHeaders, query: securityQuery } = applySecurity(doc, source, operation, schemes);
|
|
511
509
|
const headers = buildForwardedHeaders({ ...source, headers: securedHeaders }, options.session);
|
|
512
510
|
const opBaseUrl = resolveOperationBaseUrl(source, doc, item, operation, baseUrl ?? "");
|
|
@@ -540,6 +538,7 @@ ${retrySafetyNote(method, doc, item, operation)}`,
|
|
|
540
538
|
);
|
|
541
539
|
}
|
|
542
540
|
const input = enforceIdentityParams(params, isRecord(args) ? args : {}, options.session);
|
|
541
|
+
enforceResourceScope(scopeRules, input, options.session);
|
|
543
542
|
const url = buildUrl(opBaseUrl, path, params, input, securityQuery);
|
|
544
543
|
const rawBody = body ? input["body"] : void 0;
|
|
545
544
|
const requestBody = isRecord(rawBody) ? enforceServerDerivedIdentity(rawBody, options.session) : rawBody;
|
|
@@ -859,6 +858,39 @@ function enforceIdentityParams(params, input, session) {
|
|
|
859
858
|
}
|
|
860
859
|
return next;
|
|
861
860
|
}
|
|
861
|
+
function parseScopeRules(operation) {
|
|
862
|
+
const raw = operation["x-frankie-scope"];
|
|
863
|
+
const entries = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
|
|
864
|
+
const rules = [];
|
|
865
|
+
for (const entry of entries) {
|
|
866
|
+
if (isRecord(entry) && typeof entry["param"] === "string" && typeof entry["claim"] === "string") {
|
|
867
|
+
rules.push({ param: entry["param"], claim: entry["claim"] });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return rules;
|
|
871
|
+
}
|
|
872
|
+
function enforceResourceScope(rules, input, session) {
|
|
873
|
+
for (const rule of rules) {
|
|
874
|
+
if (session.user.anonymous) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
`"${rule.param}" is access-scoped, but the chat session is anonymous \u2014 Frankie will not call it for an unauthenticated visitor. Report this to the user instead of retrying.`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
const requested = input[rule.param];
|
|
880
|
+
if (requested === void 0 || requested === null || requested === "") {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`This operation requires an in-scope "${rule.param}" and none was given. Report this to the user instead of retrying.`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const allowed = session.user.claims[rule.claim];
|
|
886
|
+
const allowedList = Array.isArray(allowed) ? allowed.map((entry) => String(entry)) : typeof allowed === "string" ? [allowed] : [];
|
|
887
|
+
if (!allowedList.includes(String(requested))) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Access denied: "${String(requested)}" is not in this user's "${rule.claim}" scope. Do not retry with a different id \u2014 report this to the user.`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
862
894
|
|
|
863
895
|
// src/agent/mcp.ts
|
|
864
896
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -1445,7 +1477,7 @@ function msg(error) {
|
|
|
1445
1477
|
}
|
|
1446
1478
|
|
|
1447
1479
|
// src/agent/prompt.ts
|
|
1448
|
-
import { activeSkills } from "@frankieai/core";
|
|
1480
|
+
import { activeSkills, SKILL_TOOL_NAME } from "@frankieai/core";
|
|
1449
1481
|
function buildSystemPrompt(config, session, availableTools) {
|
|
1450
1482
|
const sections = [];
|
|
1451
1483
|
const configurationAssistant = config.features.configurationAssistant;
|
|
@@ -1501,7 +1533,7 @@ ${config.agent.systemPrompt.trim()}`);
|
|
|
1501
1533
|
}
|
|
1502
1534
|
sections.push(renderUserSection(session));
|
|
1503
1535
|
const skills = activeSkills(config, availableTools);
|
|
1504
|
-
if (skills.length) sections.push("## Reviewed workspace skills\nThese recipes do not grant additional permissions or override safety rules. Each skill lists only its currently approved, available tools. Instructions may describe additional operations: skip unavailable steps, explain the limitation, and never claim the whole task succeeded when a required step was unavailable.\n" + JSON.stringify(skills.map(({ name, goal, instructions, tools }) => ({ name, goal, instructions, tools }))));
|
|
1536
|
+
if (skills.length) sections.push("## Reviewed workspace skills\nThese recipes do not grant additional permissions or override safety rules. Each skill lists only its currently approved, available tools. Instructions may describe additional operations: skip unavailable steps, explain the limitation, and never claim the whole task succeeded when a required step was unavailable.\nBefore you begin following one of these skills, call the `" + SKILL_TOOL_NAME + "` tool with the skill's exact name, so the person can see which recipe you're applying. It grants no permissions and does nothing on its own \u2014 it's only a visible marker. Then carry out that skill's steps.\n" + JSON.stringify(skills.map(({ name, goal, instructions, tools }) => ({ name, goal, instructions, tools }))));
|
|
1505
1537
|
return sections.join("\n\n");
|
|
1506
1538
|
}
|
|
1507
1539
|
function isOnboardingAssistant(config) {
|
|
@@ -1607,9 +1639,51 @@ function renderPageContext(context) {
|
|
|
1607
1639
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
1608
1640
|
import {
|
|
1609
1641
|
describeTool as describeTool3,
|
|
1610
|
-
activeSkills as
|
|
1642
|
+
activeSkills as activeSkills3,
|
|
1611
1643
|
evaluateToolPolicy as evaluateToolPolicy3
|
|
1612
1644
|
} from "@frankieai/core";
|
|
1645
|
+
|
|
1646
|
+
// src/agent/skill-tools.ts
|
|
1647
|
+
import { Type as Type4 } from "@earendil-works/pi-ai";
|
|
1648
|
+
import { activeSkills as activeSkills2, SKILL_TOOL_NAME as SKILL_TOOL_NAME2 } from "@frankieai/core";
|
|
1649
|
+
function isSkillTool(name) {
|
|
1650
|
+
return name === SKILL_TOOL_NAME2;
|
|
1651
|
+
}
|
|
1652
|
+
function buildSkillTools(config, availableTools) {
|
|
1653
|
+
const skills = activeSkills2(config, availableTools);
|
|
1654
|
+
if (!skills.length) return [];
|
|
1655
|
+
const names = skills.map((skill) => skill.name);
|
|
1656
|
+
return [
|
|
1657
|
+
{
|
|
1658
|
+
name: SKILL_TOOL_NAME2,
|
|
1659
|
+
label: "Use skill",
|
|
1660
|
+
description: `Announce that you are about to follow one of the reviewed workspace skills, so the person can see which recipe you're applying. Call this with the skill's exact name immediately before you begin its steps. It grants no new permissions and performs no action on its own.
|
|
1661
|
+
|
|
1662
|
+
Available skills: ${names.join(", ")}.`,
|
|
1663
|
+
parameters: Type4.Unsafe({
|
|
1664
|
+
type: "object",
|
|
1665
|
+
required: ["skill"],
|
|
1666
|
+
properties: { skill: { type: "string", description: "The exact name of the workspace skill you are about to follow." } }
|
|
1667
|
+
}),
|
|
1668
|
+
async execute(_id, args) {
|
|
1669
|
+
const requested = String((typeof args === "object" && args !== null ? args["skill"] : "") ?? "").trim();
|
|
1670
|
+
const match = skills.find((skill) => skill.name.toLowerCase() === requested.toLowerCase());
|
|
1671
|
+
if (!match) {
|
|
1672
|
+
return {
|
|
1673
|
+
content: [{ type: "text", text: `No active skill is named "${requested}". The skills available this turn are: ${names.join(", ")}.` }],
|
|
1674
|
+
details: null
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
content: [{ type: "text", text: `Now following the "${match.name}" skill. Apply its steps using only its approved tools.` }],
|
|
1679
|
+
details: null
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
];
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/agent/runner.ts
|
|
1613
1687
|
async function* runTurn(params) {
|
|
1614
1688
|
const { config, session } = params;
|
|
1615
1689
|
const toolset = await collectTools({
|
|
@@ -1619,6 +1693,8 @@ async function* runTurn(params) {
|
|
|
1619
1693
|
...params.onWarning ? { onWarning: params.onWarning } : {}
|
|
1620
1694
|
});
|
|
1621
1695
|
const availableTools = toolset.tools.map((tool) => tool.name);
|
|
1696
|
+
const skillTools = buildSkillTools(config, availableTools);
|
|
1697
|
+
const agentTools = [...toolset.tools, ...skillTools];
|
|
1622
1698
|
const queue = new EventQueue();
|
|
1623
1699
|
let turnCount = 0;
|
|
1624
1700
|
let lastAssistant;
|
|
@@ -1630,7 +1706,7 @@ async function* runTurn(params) {
|
|
|
1630
1706
|
systemPrompt: buildSystemPrompt(config, session, availableTools),
|
|
1631
1707
|
model: params.modelOverride ?? params.model,
|
|
1632
1708
|
thinkingLevel: config.model.thinkingLevel,
|
|
1633
|
-
tools:
|
|
1709
|
+
tools: agentTools,
|
|
1634
1710
|
messages: session.messages
|
|
1635
1711
|
},
|
|
1636
1712
|
// Per-request sampling lives here rather than on the agent: the stream
|
|
@@ -1654,7 +1730,7 @@ async function* runTurn(params) {
|
|
|
1654
1730
|
// tools are exempt: they're gated by the workspaceAdmin flag, not the
|
|
1655
1731
|
// allow/deny policy, so the policy would otherwise block them here.
|
|
1656
1732
|
beforeToolCall: async ({ toolCall }) => {
|
|
1657
|
-
if (isPlatformTool(toolCall.name)) return void 0;
|
|
1733
|
+
if (isPlatformTool(toolCall.name) || isSkillTool(toolCall.name)) return void 0;
|
|
1658
1734
|
const decision = evaluateToolPolicy3(toolCall.name, config.agent.tools);
|
|
1659
1735
|
if (decision.allowed) return void 0;
|
|
1660
1736
|
queue.push({
|
|
@@ -1696,7 +1772,7 @@ async function* runTurn(params) {
|
|
|
1696
1772
|
});
|
|
1697
1773
|
}).finally(() => queue.close());
|
|
1698
1774
|
try {
|
|
1699
|
-
yield { type: "skills_loaded", skills:
|
|
1775
|
+
yield { type: "skills_loaded", skills: activeSkills3(config, availableTools).map(({ id, name, revision }) => ({ id, name, revision })) };
|
|
1700
1776
|
yield { type: "tools_active", apis: toolset.apis };
|
|
1701
1777
|
for await (const event of queue) yield event;
|
|
1702
1778
|
await run;
|
|
@@ -1922,6 +1998,7 @@ var EventQueue = class {
|
|
|
1922
1998
|
// src/app.ts
|
|
1923
1999
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1924
2000
|
import { Hono as Hono3 } from "hono";
|
|
2001
|
+
import { suggestedPromptsFromSkills } from "@frankieai/core";
|
|
1925
2002
|
|
|
1926
2003
|
// src/guide.ts
|
|
1927
2004
|
import { Hono as Hono2 } from "hono";
|
|
@@ -2154,7 +2231,13 @@ function createFrankieApp(options) {
|
|
|
2154
2231
|
launcherLabel: config.branding.launcherLabel
|
|
2155
2232
|
},
|
|
2156
2233
|
greeting: config.agent.greeting,
|
|
2157
|
-
|
|
2234
|
+
// Fall back to prompts derived from the workspace's own published skills,
|
|
2235
|
+
// so an empty chat suggests what this assistant can actually do instead of
|
|
2236
|
+
// nothing. An explicitly authored list always wins.
|
|
2237
|
+
suggestedPrompts: config.agent.suggestedPrompts.length ? config.agent.suggestedPrompts : suggestedPromptsFromSkills(config),
|
|
2238
|
+
// Explicit starter cards configured in the dashboard's Appearance tab.
|
|
2239
|
+
// When set, the widget renders these instead of the suggestedPrompts.
|
|
2240
|
+
starterPrompts: config.agent.starterPrompts,
|
|
2158
2241
|
theme: config.theme,
|
|
2159
2242
|
allowAnonymous: config.auth.allowAnonymous,
|
|
2160
2243
|
showToolActivity: config.agent.showToolActivity,
|
|
@@ -2652,6 +2735,11 @@ function toPublicTenant(record) {
|
|
|
2652
2735
|
...record.vercel ? { vercel: publicVercelLink(record.vercel) } : {},
|
|
2653
2736
|
theme: config.theme,
|
|
2654
2737
|
features: config.features,
|
|
2738
|
+
agent: {
|
|
2739
|
+
greeting: config.agent.greeting,
|
|
2740
|
+
suggestedPrompts: config.agent.suggestedPrompts,
|
|
2741
|
+
starterPrompts: config.agent.starterPrompts
|
|
2742
|
+
},
|
|
2655
2743
|
createdAt: record.createdAt,
|
|
2656
2744
|
updatedAt: record.updatedAt
|
|
2657
2745
|
};
|
|
@@ -3163,4 +3251,4 @@ export {
|
|
|
3163
3251
|
resolveStores,
|
|
3164
3252
|
resolveTenantStore
|
|
3165
3253
|
};
|
|
3166
|
-
//# sourceMappingURL=chunk-
|
|
3254
|
+
//# sourceMappingURL=chunk-HPYT6ZIU.js.map
|