maqam 0.1.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.
@@ -0,0 +1,119 @@
1
+ const DEFAULT_LIMITS = {
2
+ maxToolCalls: 100,
3
+ maxRuntimeMs: 600_000
4
+ };
5
+
6
+ function asSet(values = []) {
7
+ return new Set(values.filter(Boolean));
8
+ }
9
+
10
+ function isHttpUrl(value) {
11
+ try {
12
+ const url = new URL(value);
13
+ return url.protocol === "http:" || url.protocol === "https:";
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function collectUrls(value, urls = []) {
20
+ if (!value) return urls;
21
+ if (typeof value === "string") {
22
+ if (isHttpUrl(value)) urls.push(value);
23
+ return urls;
24
+ }
25
+ if (Array.isArray(value)) {
26
+ for (const item of value) collectUrls(item, urls);
27
+ return urls;
28
+ }
29
+ if (typeof value === "object") {
30
+ for (const item of Object.values(value)) collectUrls(item, urls);
31
+ }
32
+ return urls;
33
+ }
34
+
35
+ function toOrigin(value) {
36
+ try {
37
+ return new URL(value).origin;
38
+ } catch {
39
+ return value;
40
+ }
41
+ }
42
+
43
+ export class PolicyEngine {
44
+ constructor(config = {}) {
45
+ this.allowedTools = asSet(config.allowedTools);
46
+ this.deniedTools = asSet(config.deniedTools);
47
+ this.allowedOrigins = asSet((config.allowedOrigins || []).map(toOrigin));
48
+ this.deniedOrigins = asSet((config.deniedOrigins || []).map(toOrigin));
49
+ this.approvalRequiredTools = asSet(config.approvalRequiredTools);
50
+ this.defaultLimits = {
51
+ ...DEFAULT_LIMITS,
52
+ ...(config.defaultLimits || {}),
53
+ ...(config.maxToolCalls ? { maxToolCalls: config.maxToolCalls } : {})
54
+ };
55
+ }
56
+
57
+ evaluateGoal(goal = {}) {
58
+ for (const tool of goal.allowedTools || []) {
59
+ if (!this.isToolAllowed(tool)) {
60
+ return this.decision("deny", `Tool '${tool}' is not allowed for this tenant.`);
61
+ }
62
+ }
63
+
64
+ for (const origin of goal.allowedOrigins || []) {
65
+ if (!this.isOriginAllowed(toOrigin(origin))) {
66
+ return this.decision("deny", `Origin '${origin}' is not allowed for this tenant.`);
67
+ }
68
+ }
69
+
70
+ return this.decision("allow", "Goal is allowed by policy.", {
71
+ limits: {
72
+ ...this.defaultLimits,
73
+ ...(goal.budget || {})
74
+ }
75
+ });
76
+ }
77
+
78
+ authorizeToolCall({ toolName, input = {} } = {}) {
79
+ if (!this.isToolAllowed(toolName)) {
80
+ return this.decision("deny", `Tool '${toolName}' is not allowed.`);
81
+ }
82
+
83
+ if (this.approvalRequiredTools.has(toolName)) {
84
+ return this.decision("needs_approval", `Tool '${toolName}' requires approval.`, {
85
+ requiredApprovals: [`tool:${toolName}`]
86
+ });
87
+ }
88
+
89
+ const origins = [...new Set(collectUrls(input).map(toOrigin))];
90
+ for (const origin of origins) {
91
+ if (!this.isOriginAllowed(origin)) {
92
+ return this.decision("deny", `URL origin '${origin}' is not allowed.`);
93
+ }
94
+ }
95
+
96
+ return this.decision("allow", "Tool call is allowed.");
97
+ }
98
+
99
+ isToolAllowed(toolName) {
100
+ if (!toolName || this.deniedTools.has(toolName)) return false;
101
+ return this.allowedTools.size === 0 || this.allowedTools.has(toolName);
102
+ }
103
+
104
+ isOriginAllowed(origin) {
105
+ if (!origin || this.deniedOrigins.has(origin)) return false;
106
+ return this.allowedOrigins.size === 0 || this.allowedOrigins.has(origin);
107
+ }
108
+
109
+ decision(status, reason, extra = {}) {
110
+ return {
111
+ status,
112
+ reason,
113
+ limits: extra.limits || { ...this.defaultLimits },
114
+ requiredApprovals: extra.requiredApprovals || []
115
+ };
116
+ }
117
+ }
118
+
119
+ export { collectUrls };
@@ -0,0 +1,80 @@
1
+ function candidateNameFromPage(page) {
2
+ if (page.title) return page.title.replace(/\s*[-|].*$/, "").trim();
3
+ const url = new URL(page.url);
4
+ const parts = url.pathname.split("/").filter(Boolean);
5
+ return parts.at(-1) || url.hostname;
6
+ }
7
+
8
+ export function createResearchWorkflow(options = {}) {
9
+ const seeds = options.seeds || [];
10
+ const maxPages = options.maxPages || 10;
11
+
12
+ return {
13
+ name: "enterprise_research",
14
+ tasks: [
15
+ {
16
+ id: "collect_sources",
17
+ retries: 1,
18
+ run: async (context) => {
19
+ const pages = await context.tools.call("crawler", {
20
+ seeds,
21
+ maxPages,
22
+ sameOrigin: options.sameOrigin ?? true,
23
+ includeSitemaps: options.includeSitemaps ?? false
24
+ }, context);
25
+
26
+ const evidenceIds = pages.map((page) => {
27
+ const evidence = context.evidence.addEvidence({
28
+ runId: context.runId,
29
+ taskId: "collect_sources",
30
+ sourceType: "url",
31
+ source: page.url,
32
+ excerpt: page.text || page.markdown || page.title || "",
33
+ tool: "crawler",
34
+ confidence: page.status === 200 ? 0.85 : 0.5
35
+ });
36
+ return evidence.evidenceId;
37
+ });
38
+
39
+ return { pages, evidenceIds };
40
+ }
41
+ },
42
+ {
43
+ id: "synthesize_report",
44
+ run: async (context) => {
45
+ const collected = context.outputs.collect_sources || { pages: [], evidenceIds: [] };
46
+ const candidates = collected.pages.map((page, index) => {
47
+ const evidenceId = collected.evidenceIds[index];
48
+ const name = candidateNameFromPage(page);
49
+ context.evidence.addClaim({
50
+ runId: context.runId,
51
+ taskId: "synthesize_report",
52
+ text: `${name} was inspected from ${page.url}.`,
53
+ evidenceIds: [evidenceId],
54
+ confidence: 0.8
55
+ });
56
+
57
+ return {
58
+ name,
59
+ url: page.url,
60
+ whatItDoes: page.description || page.text?.slice(0, 240) || page.title || "",
61
+ whyUseful: "Potential source or reference for enterprise agent framework capabilities.",
62
+ risks: ["Requires license and maintenance review before reuse."],
63
+ recommendation: "inspiration_first",
64
+ evidenceIds: [evidenceId]
65
+ };
66
+ });
67
+
68
+ return { candidates };
69
+ }
70
+ },
71
+ {
72
+ id: "quality_checks",
73
+ run: async (context) => ({
74
+ unsupportedClaims: context.evidence.unsupportedClaims(),
75
+ evidenceCount: context.evidence.listEvidence().length
76
+ })
77
+ }
78
+ ]
79
+ };
80
+ }
@@ -0,0 +1,101 @@
1
+ import { toErrorRecord } from "./errors.js";
2
+
3
+ function withTimeout(promise, timeoutMs, taskId) {
4
+ if (!timeoutMs) return promise;
5
+ let timer;
6
+ const timeout = new Promise((_, reject) => {
7
+ timer = setTimeout(() => reject(new Error(`Task '${taskId}' timed out after ${timeoutMs}ms.`)), timeoutMs);
8
+ });
9
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
10
+ }
11
+
12
+ export class AgentRuntime {
13
+ constructor(options = {}) {
14
+ this.policyEngine = options.policyEngine || null;
15
+ this.evidenceLedger = options.evidenceLedger || null;
16
+ this.toolGateway = options.toolGateway || null;
17
+ this.clock = options.clock || (() => new Date());
18
+ }
19
+
20
+ async runWorkflow(workflow, goal = {}) {
21
+ const runId = goal.runId || `run_${this.clock().getTime()}`;
22
+ const preflight = this.policyEngine?.evaluateGoal(goal) || {
23
+ status: "allow",
24
+ reason: "No policy engine configured.",
25
+ limits: {}
26
+ };
27
+
28
+ if (preflight.status !== "allow") {
29
+ return {
30
+ runId,
31
+ status: preflight.status,
32
+ reason: preflight.reason,
33
+ trace: [],
34
+ outputs: {}
35
+ };
36
+ }
37
+
38
+ const context = {
39
+ runId,
40
+ goal,
41
+ outputs: {},
42
+ evidence: this.evidenceLedger,
43
+ tools: this.toolGateway,
44
+ trace: []
45
+ };
46
+
47
+ for (const task of workflow.tasks || []) {
48
+ const maxAttempts = 1 + (task.retries || 0);
49
+ let lastError = null;
50
+
51
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
52
+ const startedAt = this.clock().toISOString();
53
+ try {
54
+ const output = await withTimeout(
55
+ Promise.resolve(task.run(context)),
56
+ task.timeoutMs,
57
+ task.id
58
+ );
59
+ context.outputs[task.id] = output;
60
+ context.trace.push({
61
+ taskId: task.id,
62
+ status: "completed",
63
+ attempt,
64
+ startedAt,
65
+ finishedAt: this.clock().toISOString()
66
+ });
67
+ lastError = null;
68
+ break;
69
+ } catch (error) {
70
+ lastError = error;
71
+ context.trace.push({
72
+ taskId: task.id,
73
+ status: "failed",
74
+ attempt,
75
+ startedAt,
76
+ finishedAt: this.clock().toISOString(),
77
+ error: toErrorRecord(error)
78
+ });
79
+ }
80
+ }
81
+
82
+ if (lastError) {
83
+ return {
84
+ runId,
85
+ status: "failed",
86
+ error: toErrorRecord(lastError),
87
+ trace: context.trace,
88
+ outputs: context.outputs
89
+ };
90
+ }
91
+ }
92
+
93
+ return {
94
+ runId,
95
+ status: "completed",
96
+ trace: context.trace,
97
+ outputs: context.outputs,
98
+ evidence: this.evidenceLedger?.toJSON?.() || null
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,52 @@
1
+ function normalizeSkill(input) {
2
+ if (!input?.id || !input?.name || !input?.version) {
3
+ throw new TypeError("Skill requires id, name, and version.");
4
+ }
5
+ return {
6
+ id: input.id,
7
+ name: input.name,
8
+ version: input.version,
9
+ triggers: input.triggers || [],
10
+ capabilities: input.capabilities || [],
11
+ trustLevel: input.trustLevel || "community",
12
+ evalScore: Number.isFinite(Number(input.evalScore)) ? Number(input.evalScore) : 0,
13
+ metadata: input.metadata || {}
14
+ };
15
+ }
16
+
17
+ function containsAny(text, values) {
18
+ const haystack = text.toLowerCase();
19
+ return values.some((value) => haystack.includes(String(value).toLowerCase()));
20
+ }
21
+
22
+ export class SkillRegistry {
23
+ constructor() {
24
+ this.skills = new Map();
25
+ }
26
+
27
+ register(input) {
28
+ const skill = normalizeSkill(input);
29
+ this.skills.set(skill.id, skill);
30
+ return skill;
31
+ }
32
+
33
+ get(id) {
34
+ return this.skills.get(id) || null;
35
+ }
36
+
37
+ list() {
38
+ return [...this.skills.values()];
39
+ }
40
+
41
+ find(query = {}) {
42
+ const text = query.text || "";
43
+ const requiredCapabilities = query.capabilities || [];
44
+ return this.list()
45
+ .filter((skill) => {
46
+ const triggerMatch = !text || containsAny(text, skill.triggers);
47
+ const capabilityMatch = requiredCapabilities.every((capability) => skill.capabilities.includes(capability));
48
+ return triggerMatch && capabilityMatch;
49
+ })
50
+ .sort((a, b) => b.evalScore - a.evalScore || a.id.localeCompare(b.id));
51
+ }
52
+ }
@@ -0,0 +1,65 @@
1
+ import { ApprovalRequiredError, PolicyDeniedError } from "./errors.js";
2
+
3
+ export class ToolGateway {
4
+ constructor(options = {}) {
5
+ this.policyEngine = options.policyEngine || null;
6
+ this.evidenceLedger = options.evidenceLedger || null;
7
+ this.goal = options.goal || null;
8
+ this.tools = new Map();
9
+ this.trace = [];
10
+ }
11
+
12
+ registerTool(name, handler, metadata = {}) {
13
+ if (!name || typeof handler !== "function") {
14
+ throw new TypeError("ToolGateway.registerTool requires a name and handler.");
15
+ }
16
+ this.tools.set(name, { name, handler, metadata });
17
+ return this;
18
+ }
19
+
20
+ async call(toolName, input = {}, context = {}) {
21
+ const tool = this.tools.get(toolName);
22
+ if (!tool) {
23
+ throw new PolicyDeniedError(`Tool '${toolName}' is not registered.`, {
24
+ details: { toolName }
25
+ });
26
+ }
27
+
28
+ const decision = this.policyEngine?.authorizeToolCall({
29
+ goal: this.goal,
30
+ toolName,
31
+ input,
32
+ context
33
+ }) || { status: "allow", reason: "No policy engine configured.", requiredApprovals: [] };
34
+
35
+ if (decision.status === "deny") {
36
+ throw new PolicyDeniedError(decision.reason, {
37
+ details: { toolName, decision }
38
+ });
39
+ }
40
+
41
+ if (decision.status === "needs_approval") {
42
+ throw new ApprovalRequiredError(decision.reason, {
43
+ details: { toolName, requiredApprovals: decision.requiredApprovals, decision }
44
+ });
45
+ }
46
+
47
+ const startedAt = new Date().toISOString();
48
+ const result = await tool.handler(input, {
49
+ ...context,
50
+ toolName,
51
+ evidenceLedger: this.evidenceLedger
52
+ });
53
+ const finishedAt = new Date().toISOString();
54
+
55
+ this.trace.push({
56
+ toolName,
57
+ input,
58
+ startedAt,
59
+ finishedAt,
60
+ decision
61
+ });
62
+
63
+ return result;
64
+ }
65
+ }