@alashi/cli 0.15.0 → 0.16.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/assets/v1/chat.js +49 -2
- package/assets/v1/system.css +5 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +88 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{store-dipRcfKX.mjs → store-DvLX0Kft.mjs} +410 -18
- package/dist/store-DvLX0Kft.mjs.map +1 -0
- package/package.json +3 -3
- package/templates/basic-app/index.mjs +1 -1
- package/templates/basic-app/package.json +1 -1
- package/dist/store-dipRcfKX.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
3
|
-
import { APP_NAME_PATTERN, AppSdkError, ConfigError, JsonFileStore, SDK_VERSION, evaluateAppConfig, interpolateConfig, manifestConfigDefaults, mergeConfig, parseAgentFile, parseManifestWithWarnings } from "@alashi/apps-sdk";
|
|
3
|
+
import { APP_NAME_PATTERN, AppSdkError, ConfigError, JsonFileStore, SDK_VERSION, compilePolicy, describeRule, evaluateAppConfig, evaluatePolicy, interpolateConfig, manifestConfigDefaults, mergeConfig, parseAgentFile, parseManifestWithWarnings, permissionsPolicy } from "@alashi/apps-sdk";
|
|
4
4
|
import semver from "semver";
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { appendFile, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
@@ -1584,6 +1584,21 @@ function superviseSession(session, app, registry) {
|
|
|
1584
1584
|
...session.respondToApproval !== void 0 && { respondToApproval: session.respondToApproval.bind(session) }
|
|
1585
1585
|
};
|
|
1586
1586
|
}
|
|
1587
|
+
/** Runs `onClose` once when the session closes (drops its gateway grants). */
|
|
1588
|
+
function endOnClose(session, onClose) {
|
|
1589
|
+
return {
|
|
1590
|
+
get id() {
|
|
1591
|
+
return session.id;
|
|
1592
|
+
},
|
|
1593
|
+
send: (input) => session.send(input),
|
|
1594
|
+
interrupt: () => session.interrupt(),
|
|
1595
|
+
close: async () => {
|
|
1596
|
+
onClose();
|
|
1597
|
+
await session.close();
|
|
1598
|
+
},
|
|
1599
|
+
...session.respondToApproval !== void 0 && { respondToApproval: session.respondToApproval.bind(session) }
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1587
1602
|
/**
|
|
1588
1603
|
* Wraps the host provider so every session an app opens automatically gets
|
|
1589
1604
|
* its manifest-declared MCP servers, tool allowlist, agents, skills,
|
|
@@ -1600,6 +1615,30 @@ function scopeProvider(provider, defaults) {
|
|
|
1600
1615
|
...defaults.mcpServers,
|
|
1601
1616
|
...options.mcpServers
|
|
1602
1617
|
};
|
|
1618
|
+
if (defaults.gateSession) {
|
|
1619
|
+
const { allowedTools: callAllowed, askTools: callAsk, ...rest } = options;
|
|
1620
|
+
const gate = defaults.gateSession({
|
|
1621
|
+
...callAllowed !== void 0 && { allowedTools: callAllowed },
|
|
1622
|
+
...callAsk !== void 0 && { askTools: callAsk }
|
|
1623
|
+
});
|
|
1624
|
+
const agents = {
|
|
1625
|
+
...defaults.agents,
|
|
1626
|
+
...rest.agents
|
|
1627
|
+
};
|
|
1628
|
+
const skillDirs = [.../* @__PURE__ */ new Set([...defaults.skillDirs, ...rest.skillDirs ?? []])];
|
|
1629
|
+
let session = await provider.createSession({
|
|
1630
|
+
cwd: defaults.cwd,
|
|
1631
|
+
...defaults.instructions !== void 0 && { systemPrompt: defaults.instructions },
|
|
1632
|
+
...rest,
|
|
1633
|
+
...Object.keys(mcpServers).length > 0 && { mcpServers },
|
|
1634
|
+
toolGate: gate.gate,
|
|
1635
|
+
...Object.keys(agents).length > 0 && { agents },
|
|
1636
|
+
...skillDirs.length > 0 && { skillDirs }
|
|
1637
|
+
});
|
|
1638
|
+
session = endOnClose(session, gate.end);
|
|
1639
|
+
if (defaults.supervise) session = defaults.supervise(session);
|
|
1640
|
+
return defaults.recordUsage ? meterSession(session, defaults.recordUsage) : session;
|
|
1641
|
+
}
|
|
1603
1642
|
const granted = (defaults.readGrants ? await defaults.readGrants() : []).filter((tool) => defaults.askTools.includes(tool));
|
|
1604
1643
|
const allowedTools = [.../* @__PURE__ */ new Set([
|
|
1605
1644
|
...defaults.allowedTools,
|
|
@@ -1717,7 +1756,7 @@ async function resolveAppConfig(store, manifest) {
|
|
|
1717
1756
|
* `ctx.usage` answers "nothing recorded" — the member is required by the
|
|
1718
1757
|
* contract, so it is always present.
|
|
1719
1758
|
*/
|
|
1720
|
-
async function buildAppContext(store, manifest, provider, usage, approvals) {
|
|
1759
|
+
async function buildAppContext(store, manifest, provider, usage, approvals, gateway) {
|
|
1721
1760
|
const name = manifest.name;
|
|
1722
1761
|
const dataDir = store.appDataDir(name);
|
|
1723
1762
|
const repoDir = store.appRepoDir(name);
|
|
@@ -1735,7 +1774,35 @@ async function buildAppContext(store, manifest, provider, usage, approvals) {
|
|
|
1735
1774
|
const allowedTools = manifest.permissions.allowedTools.filter((tool) => !excluded.some((server) => tool.startsWith(`mcp__${server}__`)));
|
|
1736
1775
|
const logger = createLogger(name);
|
|
1737
1776
|
let askTools = manifest.permissions.askTools.filter((tool) => !excluded.some((server) => tool.startsWith(`mcp__${server}__`)));
|
|
1738
|
-
|
|
1777
|
+
const agents = await loadAgents(repoDir, manifest.agents);
|
|
1778
|
+
const gated = gateway !== void 0 && provider.supportsToolGate === true && provider.supportsApprovals === true;
|
|
1779
|
+
const declaresPolicy = manifest.permissions.rules.length > 0 || manifest.permissions.default !== "deny";
|
|
1780
|
+
let gateSession;
|
|
1781
|
+
if (gated) {
|
|
1782
|
+
const base = permissionsPolicy({
|
|
1783
|
+
...manifest.permissions,
|
|
1784
|
+
allowedTools,
|
|
1785
|
+
askTools
|
|
1786
|
+
});
|
|
1787
|
+
const implicit = Object.keys(agents).length > 0 ? [{
|
|
1788
|
+
id: "implicit.Agent",
|
|
1789
|
+
tool: "Agent",
|
|
1790
|
+
decision: "allow"
|
|
1791
|
+
}] : [];
|
|
1792
|
+
gateSession = (extra) => gateway.session(name, compilePolicy({
|
|
1793
|
+
default: base.default,
|
|
1794
|
+
rules: [
|
|
1795
|
+
...base.rules,
|
|
1796
|
+
...implicit,
|
|
1797
|
+
...permissionsPolicy(extra).rules
|
|
1798
|
+
]
|
|
1799
|
+
}));
|
|
1800
|
+
} else if (declaresPolicy) {
|
|
1801
|
+
logger.warn(`AI engine "${provider.name}" cannot enforce policy rules; all tools are disabled for this app`);
|
|
1802
|
+
allowedTools.length = 0;
|
|
1803
|
+
askTools = [];
|
|
1804
|
+
}
|
|
1805
|
+
if (!gated && askTools.length > 0 && provider.supportsApprovals !== true) {
|
|
1739
1806
|
logger.warn(`AI engine "${provider.name}" does not support interactive approvals; ask-tier tools are disabled`, { askTools });
|
|
1740
1807
|
askTools = [];
|
|
1741
1808
|
}
|
|
@@ -1757,11 +1824,10 @@ async function buildAppContext(store, manifest, provider, usage, approvals) {
|
|
|
1757
1824
|
mcpServers,
|
|
1758
1825
|
allowedTools,
|
|
1759
1826
|
askTools,
|
|
1760
|
-
...
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
agents: await loadAgents(repoDir, manifest.agents),
|
|
1827
|
+
...gateSession !== void 0 && { gateSession },
|
|
1828
|
+
...(gated || askTools.length > 0) && approvals !== void 0 && { supervise: (session) => superviseSession(session, name, approvals) },
|
|
1829
|
+
...!gated && askTools.length > 0 && { readGrants: async () => (await store.readGrants(name)).tools },
|
|
1830
|
+
agents,
|
|
1765
1831
|
skillDirs: manifest.skills.map((dir) => resolve(repoDir, dir)),
|
|
1766
1832
|
...instructions !== void 0 && { instructions },
|
|
1767
1833
|
cwd: dataDir,
|
|
@@ -1814,7 +1880,9 @@ var ApprovalRegistry = class {
|
|
|
1814
1880
|
input: event.input,
|
|
1815
1881
|
requestedAt: new Date(now).toISOString(),
|
|
1816
1882
|
expiresAt: new Date(now + this.#timeoutMs).toISOString(),
|
|
1817
|
-
status: "pending"
|
|
1883
|
+
status: "pending",
|
|
1884
|
+
...event.ruleId !== void 0 && { ruleId: event.ruleId },
|
|
1885
|
+
...event.reason !== void 0 && { reason: event.reason }
|
|
1818
1886
|
};
|
|
1819
1887
|
const timer = setTimeout(() => this.#timeout(event.approvalId), this.#timeoutMs);
|
|
1820
1888
|
timer.unref?.();
|
|
@@ -2477,6 +2545,45 @@ function roCard(title, children) {
|
|
|
2477
2545
|
]);
|
|
2478
2546
|
}
|
|
2479
2547
|
|
|
2548
|
+
// Every tool call passes the host's tool gateway: user overrides first
|
|
2549
|
+
// (never past a manifest deny), then the manifest's rules, then the default.
|
|
2550
|
+
function ruleRow(r) {
|
|
2551
|
+
return el('div', { class: 'ro-item' }, [
|
|
2552
|
+
el('span', { class: 'chip ' + (r.decision === 'allow' ? 'on' : r.decision === 'deny' ? 'off' : ''),
|
|
2553
|
+
text: r.decision }),
|
|
2554
|
+
document.createTextNode(' '),
|
|
2555
|
+
el('code', { text: r.summary }),
|
|
2556
|
+
el('div', { class: 'desc', text: (r.description && r.description !== r.summary ? r.description + ' · ' : '') + 'id: ' + r.id }),
|
|
2557
|
+
]);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function policyCard(p) {
|
|
2561
|
+
const body = [];
|
|
2562
|
+
if (!p.gated) {
|
|
2563
|
+
body.push(el('div', { class: 'desc', text: 'This AI engine cannot gate individual tool calls; only name-level allow/ask lists apply.' }));
|
|
2564
|
+
}
|
|
2565
|
+
if (p.user.length > 0) {
|
|
2566
|
+
body.push(el('h3', { text: 'Your overrides' }), ...p.user.map(ruleRow));
|
|
2567
|
+
}
|
|
2568
|
+
body.push(el('h3', { text: 'Declared by the app' }));
|
|
2569
|
+
body.push(...(p.manifest.length > 0 ? p.manifest.map(ruleRow) : [el('div', { class: 'desc', text: 'No rules.' })]));
|
|
2570
|
+
body.push(el('div', { class: 'desc', text: 'Anything else: ' + p.default + '.' }));
|
|
2571
|
+
return roCard('Tool policy', body);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function toolCallsCard(calls) {
|
|
2575
|
+
return roCard('Recent tool calls', [usageTable(
|
|
2576
|
+
['When', 'Tool', 'Decision', 'Why', 'Input'],
|
|
2577
|
+
calls.slice().reverse().map((c) => [
|
|
2578
|
+
new Date(c.at).toLocaleString(),
|
|
2579
|
+
c.tool,
|
|
2580
|
+
c.decision + ' (' + c.source + ')',
|
|
2581
|
+
c.reason,
|
|
2582
|
+
c.inputPreview.length > 80 ? c.inputPreview.slice(0, 80) + '…' : c.inputPreview,
|
|
2583
|
+
]),
|
|
2584
|
+
)]);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2480
2587
|
function renderManifest(m) {
|
|
2481
2588
|
const root = document.getElementById('manifest-cards');
|
|
2482
2589
|
root.replaceChildren();
|
|
@@ -2522,10 +2629,8 @@ function renderManifest(m) {
|
|
|
2522
2629
|
]))));
|
|
2523
2630
|
}
|
|
2524
2631
|
|
|
2525
|
-
if (m.
|
|
2526
|
-
|
|
2527
|
-
el('div', {}, [el('code', { text: t })]))));
|
|
2528
|
-
}
|
|
2632
|
+
if (m.policy) cards.push(policyCard(m.policy));
|
|
2633
|
+
if (m.toolCalls && m.toolCalls.length > 0) cards.push(toolCallsCard(m.toolCalls));
|
|
2529
2634
|
|
|
2530
2635
|
if (m.jobs.length > 0) {
|
|
2531
2636
|
cards.push(roCard('Jobs', m.jobs.map((j) => el('div', { class: 'ro-item' }, [
|
|
@@ -2559,7 +2664,16 @@ async function load() {
|
|
|
2559
2664
|
return;
|
|
2560
2665
|
}
|
|
2561
2666
|
render(await res.json());
|
|
2562
|
-
if (manRes.ok)
|
|
2667
|
+
if (manRes.ok) {
|
|
2668
|
+
const m = await manRes.json();
|
|
2669
|
+
const [policyRes, callsRes] = await Promise.all([
|
|
2670
|
+
fetch('/api/apps/' + APP + '/policy'),
|
|
2671
|
+
fetch('/api/apps/' + APP + '/tool-calls?limit=20'),
|
|
2672
|
+
]);
|
|
2673
|
+
if (policyRes.ok) m.policy = await policyRes.json();
|
|
2674
|
+
if (callsRes.ok) m.toolCalls = (await callsRes.json()).calls;
|
|
2675
|
+
renderManifest(m);
|
|
2676
|
+
}
|
|
2563
2677
|
}
|
|
2564
2678
|
|
|
2565
2679
|
document.getElementById('save').addEventListener('click', async () => {
|
|
@@ -2587,6 +2701,193 @@ load();
|
|
|
2587
2701
|
`;
|
|
2588
2702
|
}
|
|
2589
2703
|
//#endregion
|
|
2704
|
+
//#region src/tool-gateway.ts
|
|
2705
|
+
const INPUT_PREVIEW_CHARS = 1e3;
|
|
2706
|
+
/** Calls the gate answered `ask` and nobody has decided yet, bounded per host. */
|
|
2707
|
+
const PENDING_ASK_CAP = 500;
|
|
2708
|
+
/** The audit ledger is trimmed to this many newest lines... */
|
|
2709
|
+
const AUDIT_KEEP = 5e3;
|
|
2710
|
+
/** ...checked once every this many appends per app. */
|
|
2711
|
+
const AUDIT_TRIM_EVERY = 500;
|
|
2712
|
+
/**
|
|
2713
|
+
* The host's tool gateway: every tool call any app session makes — chat,
|
|
2714
|
+
* cron, app routes; built-in, MCP, subagent — is decided here (see
|
|
2715
|
+
* `SessionOptions.toolGate`). Layers, in order:
|
|
2716
|
+
*
|
|
2717
|
+
* 1. the manifest policy (rules, then `allowedTools`/`askTools` sugar);
|
|
2718
|
+
* 2. the user's overrides (`policy.json`, plus legacy `grants.json`) — which
|
|
2719
|
+
* may tighten anything but never loosen past a manifest `deny`;
|
|
2720
|
+
* 3. session grants ("approve for session"), keyed by the rule that asked;
|
|
2721
|
+
*
|
|
2722
|
+
* and every decision is appended to the app's `tool-calls.jsonl` audit
|
|
2723
|
+
* ledger. `ask` verdicts go on to the provider's interactive approval flow;
|
|
2724
|
+
* `applyDecision` turns the user's answer into a session grant or a
|
|
2725
|
+
* persistent user rule.
|
|
2726
|
+
*/
|
|
2727
|
+
var ToolGateway = class {
|
|
2728
|
+
#store;
|
|
2729
|
+
#sessionGrants = /* @__PURE__ */ new Map();
|
|
2730
|
+
#pendingAsks = /* @__PURE__ */ new Map();
|
|
2731
|
+
#appendCounts = /* @__PURE__ */ new Map();
|
|
2732
|
+
/** Serializes ledger appends/trims and policy.json writes. */
|
|
2733
|
+
#queue = Promise.resolve();
|
|
2734
|
+
constructor(store) {
|
|
2735
|
+
this.#store = store;
|
|
2736
|
+
}
|
|
2737
|
+
/** Builds the gate for one new session of `app`. */
|
|
2738
|
+
session(app, manifestPolicy) {
|
|
2739
|
+
const sessionKey = randomUUID();
|
|
2740
|
+
const gate = async (call) => this.#decide(app, sessionKey, manifestPolicy, call);
|
|
2741
|
+
return {
|
|
2742
|
+
gate,
|
|
2743
|
+
end: () => this.#sessionGrants.delete(sessionKey)
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
/** The user override layer as stored: policy.json rules, then legacy grants. */
|
|
2747
|
+
async userPolicy(app) {
|
|
2748
|
+
const [policy, grants] = await Promise.all([this.#store.readUserPolicy(app), this.#store.readGrants(app)]);
|
|
2749
|
+
return { rules: [...policy.rules, ...grants.tools.map((tool) => ({
|
|
2750
|
+
id: `grants.${tool}`,
|
|
2751
|
+
tool,
|
|
2752
|
+
decision: "allow"
|
|
2753
|
+
}))] };
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Applies the scope of a user's answer to a call this gateway sent to
|
|
2757
|
+
* `ask`: `session` grants the asking rule for the rest of the session,
|
|
2758
|
+
* `always` appends an allow rule with the asking rule's scope to the app's
|
|
2759
|
+
* policy.json. Returns false for approvals this gateway never saw (legacy
|
|
2760
|
+
* name-level sessions), so the caller can fall back.
|
|
2761
|
+
*/
|
|
2762
|
+
async applyDecision(approvalId, decision) {
|
|
2763
|
+
const pending = this.#pendingAsks.get(approvalId);
|
|
2764
|
+
if (!pending) return false;
|
|
2765
|
+
this.#pendingAsks.delete(approvalId);
|
|
2766
|
+
if (decision.behavior !== "allow") return true;
|
|
2767
|
+
if (decision.scope === "session" || decision.scope === "always") {
|
|
2768
|
+
let grants = this.#sessionGrants.get(pending.sessionKey);
|
|
2769
|
+
if (!grants) {
|
|
2770
|
+
grants = /* @__PURE__ */ new Set();
|
|
2771
|
+
this.#sessionGrants.set(pending.sessionKey, grants);
|
|
2772
|
+
}
|
|
2773
|
+
grants.add(pending.grantKey);
|
|
2774
|
+
}
|
|
2775
|
+
if (decision.scope === "always") await this.#persistAlways(pending);
|
|
2776
|
+
return true;
|
|
2777
|
+
}
|
|
2778
|
+
/** The newest audit lines for an app, newest last. */
|
|
2779
|
+
async recentCalls(app, limit = 100) {
|
|
2780
|
+
await this.flush();
|
|
2781
|
+
return (await this.#store.readToolCalls(app)).slice(-limit);
|
|
2782
|
+
}
|
|
2783
|
+
/** Resolves when queued ledger and policy writes have landed (tests). */
|
|
2784
|
+
flush() {
|
|
2785
|
+
return this.#queue.then(() => void 0);
|
|
2786
|
+
}
|
|
2787
|
+
async #decide(app, sessionKey, manifestPolicy, call) {
|
|
2788
|
+
const manifest = evaluatePolicy(manifestPolicy, call);
|
|
2789
|
+
let verdict = manifest;
|
|
2790
|
+
let source = manifest.ruleId !== void 0 ? "manifest" : "default";
|
|
2791
|
+
if (manifest.decision !== "deny") {
|
|
2792
|
+
const user = evaluatePolicy(compilePolicy({
|
|
2793
|
+
default: "deny",
|
|
2794
|
+
...await this.userPolicy(app)
|
|
2795
|
+
}), call);
|
|
2796
|
+
if (user.ruleId !== void 0) {
|
|
2797
|
+
verdict = user;
|
|
2798
|
+
source = "user";
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
let result;
|
|
2802
|
+
if (verdict.decision === "ask") {
|
|
2803
|
+
const grantKey = verdict.ruleId ?? `default:${call.tool}`;
|
|
2804
|
+
if (this.#sessionGrants.get(sessionKey)?.has(grantKey)) {
|
|
2805
|
+
source = "session";
|
|
2806
|
+
result = { decision: "allow" };
|
|
2807
|
+
} else {
|
|
2808
|
+
this.#trackAsk(call.toolUseId, {
|
|
2809
|
+
app,
|
|
2810
|
+
sessionKey,
|
|
2811
|
+
grantKey,
|
|
2812
|
+
tool: call.tool,
|
|
2813
|
+
...verdict.rule !== void 0 && { rule: verdict.rule }
|
|
2814
|
+
});
|
|
2815
|
+
result = {
|
|
2816
|
+
decision: "ask",
|
|
2817
|
+
...verdict.ruleId !== void 0 && { ruleId: verdict.ruleId },
|
|
2818
|
+
reason: verdict.reason
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
} else if (verdict.decision === "allow") result = { decision: "allow" };
|
|
2822
|
+
else result = {
|
|
2823
|
+
decision: "deny",
|
|
2824
|
+
reason: `Denied by policy: ${verdict.reason}`
|
|
2825
|
+
};
|
|
2826
|
+
this.#audit(app, {
|
|
2827
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2828
|
+
tool: call.tool,
|
|
2829
|
+
toolUseId: call.toolUseId,
|
|
2830
|
+
inputPreview: preview(call.input),
|
|
2831
|
+
decision: result.decision,
|
|
2832
|
+
source,
|
|
2833
|
+
...verdict.ruleId !== void 0 && { ruleId: verdict.ruleId },
|
|
2834
|
+
reason: source === "session" ? `session grant: ${verdict.reason}` : verdict.reason
|
|
2835
|
+
});
|
|
2836
|
+
return result;
|
|
2837
|
+
}
|
|
2838
|
+
#trackAsk(toolUseId, pending) {
|
|
2839
|
+
this.#pendingAsks.set(toolUseId, pending);
|
|
2840
|
+
if (this.#pendingAsks.size > PENDING_ASK_CAP) {
|
|
2841
|
+
const oldest = this.#pendingAsks.keys().next().value;
|
|
2842
|
+
if (oldest !== void 0) this.#pendingAsks.delete(oldest);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
#persistAlways(pending) {
|
|
2846
|
+
const scope = pending.rule ?? {
|
|
2847
|
+
tool: pending.tool,
|
|
2848
|
+
decision: "ask"
|
|
2849
|
+
};
|
|
2850
|
+
const rule = {
|
|
2851
|
+
id: `always.${pending.grantKey}`,
|
|
2852
|
+
tool: scope.tool,
|
|
2853
|
+
...scope.input !== void 0 && { input: scope.input },
|
|
2854
|
+
decision: "allow",
|
|
2855
|
+
description: `Always allowed: ${describeRule(scope)}`
|
|
2856
|
+
};
|
|
2857
|
+
const write = this.#queue.then(async () => {
|
|
2858
|
+
const policy = await this.#store.readUserPolicy(pending.app);
|
|
2859
|
+
if (policy.rules.some((existing) => existing.id === rule.id)) return;
|
|
2860
|
+
policy.rules.unshift(rule);
|
|
2861
|
+
await this.#store.writeUserPolicy(pending.app, policy);
|
|
2862
|
+
});
|
|
2863
|
+
this.#queue = write.catch(() => void 0);
|
|
2864
|
+
return write;
|
|
2865
|
+
}
|
|
2866
|
+
#audit(app, record) {
|
|
2867
|
+
const write = this.#queue.then(async () => {
|
|
2868
|
+
await this.#store.appendToolCall(app, record);
|
|
2869
|
+
const count = (this.#appendCounts.get(app) ?? 0) + 1;
|
|
2870
|
+
this.#appendCounts.set(app, count);
|
|
2871
|
+
if (count % AUDIT_TRIM_EVERY === 0) {
|
|
2872
|
+
const records = await this.#store.readToolCalls(app);
|
|
2873
|
+
if (records.length > AUDIT_KEEP) await this.#store.writeToolCalls(app, records.slice(-5e3));
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
this.#queue = write.catch((err) => {
|
|
2877
|
+
console.warn(`alashi: failed to write tool-call audit for "${app}": ${String(err)}`);
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
function preview(input) {
|
|
2882
|
+
let json;
|
|
2883
|
+
try {
|
|
2884
|
+
json = JSON.stringify(input) ?? String(input);
|
|
2885
|
+
} catch {
|
|
2886
|
+
json = String(input);
|
|
2887
|
+
}
|
|
2888
|
+
return json.length > INPUT_PREVIEW_CHARS ? `${json.slice(0, INPUT_PREVIEW_CHARS)}…` : json;
|
|
2889
|
+
}
|
|
2890
|
+
//#endregion
|
|
2590
2891
|
//#region src/server.ts
|
|
2591
2892
|
const CONTENT_TYPES = {
|
|
2592
2893
|
".html": "text/html; charset=utf-8",
|
|
@@ -2689,8 +2990,11 @@ function appErrorPage(name, err) {
|
|
|
2689
2990
|
* optionally adopting an existing provider session (e.g. an
|
|
2690
2991
|
* app-run investigation) so chat continues where it left off
|
|
2691
2992
|
* GET /api/apps/:name/chat/:conversationId/history past turns from the provider transcript
|
|
2993
|
+
* plus the conversation's settled approval records
|
|
2692
2994
|
* GET /api/apps/:name/jobs manifest-declared jobs with schedule/status
|
|
2693
2995
|
* POST /api/apps/:name/jobs/:job/run trigger a job now (409 while already running)
|
|
2996
|
+
* GET /api/apps/:name/policy effective tool policy: user overrides + manifest rules
|
|
2997
|
+
* GET /api/apps/:name/tool-calls?limit= newest gated tool calls (audit ledger)
|
|
2694
2998
|
* GET /api/apps/:name/manifest read-only app definition: instructions, agents,
|
|
2695
2999
|
* skills (with dir contents), allowed tools,
|
|
2696
3000
|
* MCP servers (with configured state), jobs, model
|
|
@@ -2709,6 +3013,7 @@ function createServer(store, provider, options = {}) {
|
|
|
2709
3013
|
const sessions = /* @__PURE__ */ new Map();
|
|
2710
3014
|
const usage = new UsageRecorder(store);
|
|
2711
3015
|
const approvals = new ApprovalRegistry(store);
|
|
3016
|
+
const gateway = new ToolGateway(store);
|
|
2712
3017
|
approvals.init().catch((err) => {
|
|
2713
3018
|
console.warn(`alashi: failed to read approval timeout config: ${String(err)}`);
|
|
2714
3019
|
});
|
|
@@ -2719,7 +3024,7 @@ function createServer(store, provider, options = {}) {
|
|
|
2719
3024
|
});
|
|
2720
3025
|
async function startApp(name) {
|
|
2721
3026
|
const { definition, manifest } = await loadApp(store.appRepoDir(name));
|
|
2722
|
-
const ctx = await buildAppContext(store, manifest, provider, usage, approvals);
|
|
3027
|
+
const ctx = await buildAppContext(store, manifest, provider, usage, approvals, gateway);
|
|
2723
3028
|
return {
|
|
2724
3029
|
manifest,
|
|
2725
3030
|
ctx,
|
|
@@ -3073,7 +3378,12 @@ ${approvalsSection}
|
|
|
3073
3378
|
const convo = conversations.find((conv) => conv.id === conversationId);
|
|
3074
3379
|
if (!convo) return c.json({ error: "Unknown conversation" }, 404);
|
|
3075
3380
|
const messages = convo.sessionId ? await provider.history?.(convo.sessionId) ?? [] : [];
|
|
3076
|
-
|
|
3381
|
+
await approvals.flush();
|
|
3382
|
+
const settled = (await store.readApprovalLog(name)).approvals.filter((entry) => entry.conversationId === conversationId && entry.status !== "pending");
|
|
3383
|
+
return c.json({
|
|
3384
|
+
messages,
|
|
3385
|
+
approvals: settled
|
|
3386
|
+
});
|
|
3077
3387
|
});
|
|
3078
3388
|
/** Drops the cached app and its warm sessions so the next request rebuilds with fresh config. */
|
|
3079
3389
|
function restartApp(name) {
|
|
@@ -3084,6 +3394,33 @@ ${approvalsSection}
|
|
|
3084
3394
|
session.then((s) => s.close()).catch(() => void 0);
|
|
3085
3395
|
}
|
|
3086
3396
|
}
|
|
3397
|
+
app.get("/api/apps/:name/policy", async (c) => {
|
|
3398
|
+
const name = c.req.param("name");
|
|
3399
|
+
const running = await getApp(name);
|
|
3400
|
+
if (!running) return c.json({ error: `App "${name}" is not installed` }, 404);
|
|
3401
|
+
const manifestPolicy = permissionsPolicy(running.manifest.permissions);
|
|
3402
|
+
const user = await gateway.userPolicy(name);
|
|
3403
|
+
const describe = (rule) => ({
|
|
3404
|
+
...rule,
|
|
3405
|
+
summary: describeRule(rule)
|
|
3406
|
+
});
|
|
3407
|
+
return c.json({
|
|
3408
|
+
gated: provider.supportsToolGate === true && provider.supportsApprovals === true,
|
|
3409
|
+
default: manifestPolicy.default,
|
|
3410
|
+
user: user.rules.map(describe),
|
|
3411
|
+
manifest: manifestPolicy.rules.map((rule, index) => describe({
|
|
3412
|
+
...rule,
|
|
3413
|
+
id: rule.id ?? `rules.${index}`
|
|
3414
|
+
}))
|
|
3415
|
+
});
|
|
3416
|
+
});
|
|
3417
|
+
app.get("/api/apps/:name/tool-calls", async (c) => {
|
|
3418
|
+
const name = c.req.param("name");
|
|
3419
|
+
if (!await getApp(name)) return c.json({ error: `App "${name}" is not installed` }, 404);
|
|
3420
|
+
const raw = Number(c.req.query("limit") ?? 100);
|
|
3421
|
+
const limit = Number.isInteger(raw) && raw > 0 ? Math.min(raw, 1e3) : 100;
|
|
3422
|
+
return c.json({ calls: await gateway.recentCalls(name, limit) });
|
|
3423
|
+
});
|
|
3087
3424
|
app.get("/api/apps/:name/manifest", async (c) => {
|
|
3088
3425
|
const name = c.req.param("name");
|
|
3089
3426
|
const running = await getApp(name);
|
|
@@ -3314,7 +3651,13 @@ ${approvalsSection}
|
|
|
3314
3651
|
if (!decision) return c.json({ error: "Body must be { \"behavior\": \"allow\"|\"deny\", \"scope\"?, \"message\"? }" }, 400);
|
|
3315
3652
|
const record = approvals.get(approvalId);
|
|
3316
3653
|
if (!record || expect !== void 0 && (record.app !== expect.app || expect.conversationId !== void 0 && record.conversationId !== expect.conversationId)) return c.json({ error: `Unknown approval "${approvalId}"` }, 404);
|
|
3317
|
-
|
|
3654
|
+
let gatewayHandled;
|
|
3655
|
+
try {
|
|
3656
|
+
gatewayHandled = record.status === "pending" && await gateway.applyDecision(approvalId, decision);
|
|
3657
|
+
} catch (err) {
|
|
3658
|
+
return c.json({ error: `Failed to persist grant: ${String(err)}` }, 500);
|
|
3659
|
+
}
|
|
3660
|
+
if (!gatewayHandled && decision.behavior === "allow" && decision.scope === "always") {
|
|
3318
3661
|
const running = await getApp(record.app);
|
|
3319
3662
|
if (running) try {
|
|
3320
3663
|
await approvals.addGrant(record.app, record.tool, running.manifest.permissions.askTools);
|
|
@@ -3447,6 +3790,12 @@ var Store = class {
|
|
|
3447
3790
|
appGrantsPath(name) {
|
|
3448
3791
|
return join(this.appDir(name), "grants.json");
|
|
3449
3792
|
}
|
|
3793
|
+
appPolicyPath(name) {
|
|
3794
|
+
return join(this.appDir(name), "policy.json");
|
|
3795
|
+
}
|
|
3796
|
+
appToolCallsPath(name) {
|
|
3797
|
+
return join(this.appDir(name), "tool-calls.jsonl");
|
|
3798
|
+
}
|
|
3450
3799
|
appUsagePath(name) {
|
|
3451
3800
|
return join(this.appDir(name), "usage.json");
|
|
3452
3801
|
}
|
|
@@ -3493,6 +3842,49 @@ var Store = class {
|
|
|
3493
3842
|
async writeGrants(name, state) {
|
|
3494
3843
|
await writeJson(this.appGrantsPath(name), state);
|
|
3495
3844
|
}
|
|
3845
|
+
async readUserPolicy(name) {
|
|
3846
|
+
const state = await readJson(this.appPolicyPath(name));
|
|
3847
|
+
return { rules: Array.isArray(state?.rules) ? state.rules : [] };
|
|
3848
|
+
}
|
|
3849
|
+
async writeUserPolicy(name, policy) {
|
|
3850
|
+
await writeJson(this.appPolicyPath(name), policy);
|
|
3851
|
+
}
|
|
3852
|
+
/** Appends one line to the app's tool-call audit ledger. */
|
|
3853
|
+
async appendToolCall(name, record) {
|
|
3854
|
+
const path = this.appToolCallsPath(name);
|
|
3855
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
3856
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Parseable lines of the tool-call ledger, oldest first. Like the turn
|
|
3860
|
+
* ledger, a truncated or hand-mangled line costs only itself.
|
|
3861
|
+
*/
|
|
3862
|
+
async readToolCalls(name) {
|
|
3863
|
+
let content;
|
|
3864
|
+
try {
|
|
3865
|
+
content = await readFile(this.appToolCallsPath(name), "utf8");
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
if (err.code === "ENOENT") return [];
|
|
3868
|
+
throw err;
|
|
3869
|
+
}
|
|
3870
|
+
const records = [];
|
|
3871
|
+
for (const line of content.split("\n")) {
|
|
3872
|
+
if (line.trim() === "") continue;
|
|
3873
|
+
try {
|
|
3874
|
+
const record = JSON.parse(line);
|
|
3875
|
+
if (typeof record?.at === "string" && typeof record.tool === "string") records.push(record);
|
|
3876
|
+
} catch {}
|
|
3877
|
+
}
|
|
3878
|
+
return records;
|
|
3879
|
+
}
|
|
3880
|
+
/** Replaces the tool-call ledger atomically (tmp file + rename), for trimming. */
|
|
3881
|
+
async writeToolCalls(name, records) {
|
|
3882
|
+
const path = this.appToolCallsPath(name);
|
|
3883
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
3884
|
+
const tmp = `${path}.tmp`;
|
|
3885
|
+
await writeFile(tmp, records.map((r) => `${JSON.stringify(r)}\n`).join(""), "utf8");
|
|
3886
|
+
await rename(tmp, path);
|
|
3887
|
+
}
|
|
3496
3888
|
async readJobState(name) {
|
|
3497
3889
|
const state = await readJson(this.appJobStatePath(name));
|
|
3498
3890
|
return {
|
|
@@ -3591,4 +3983,4 @@ var Store = class {
|
|
|
3591
3983
|
//#endregion
|
|
3592
3984
|
export { UsageRecorder as A, BudgetExceededError as C, budgetStatusFor as D, budgetStatus as E, createAuthMiddleware as F, isConsoleRequest as I, registerAuthRoutes as L, utcDay as M, AuthService as N, setAppBudget as O, SESSION_COOKIE as P, requiredLevel as R, BUDGET_EXCEEDED_CODE as S, assertWithinBudget as T, fetchSource as _, EXPORT_FORMAT_VERSION as a, loadApp as b, installApp as c, updateApp as d, addRegistry as f, resolveApp as g, removeRegistry as h, resolveAppConfig as i, formatUsd as j, USAGE_RETENTION_DAYS as k, installDependencies as l, refreshRegistry as m, createServer as n, exportApp as o, readCachedIndex as p, buildAppContext as r, importApp as s, Store as t, removeApp as u, parseSourceArg as v, appBudget as w, checkSdkCompatibility as x, updateSource as y };
|
|
3593
3985
|
|
|
3594
|
-
//# sourceMappingURL=store-
|
|
3986
|
+
//# sourceMappingURL=store-DvLX0Kft.mjs.map
|