maqam 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -10,6 +10,8 @@ Full documentation: [docs/usage.md](https://github.com/AjnasNB/maqam/blob/main/d
10
10
 
11
11
  ![Maqam system map](app/assets/maqam-system-map.svg)
12
12
 
13
+ ![Maqam governed CLI worker flow](app/assets/maqam-cli-agent-flow.png)
14
+
13
15
  ## What Ships
14
16
 
15
17
  - `AgentRuntime`: sequential workflow execution with retries, trace events, task outputs, and policy preflight.
@@ -17,6 +19,7 @@ Full documentation: [docs/usage.md](https://github.com/AjnasNB/maqam/blob/main/d
17
19
  - `EvidenceLedger`: provenance records, claim links, source hashes, confidence, and unsupported-claim checks.
18
20
  - `ToolGateway`: one governed path for external tool execution.
19
21
  - `createAgentTool`: wraps any function agent or object agent so Maqam can control it through policy, trace, approval, and evidence.
22
+ - `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
20
23
  - `SkillRegistry`: lightweight skill metadata registration and selection.
21
24
  - `createResearchWorkflow`: crawler-backed source collection, synthesis, and quality checks.
22
25
  - `maqam`: local web console for running governed research workflows.
@@ -82,6 +85,7 @@ import {
82
85
  PolicyEngine,
83
86
  ToolGateway,
84
87
  createAgentTool,
88
+ createCliAgentTool,
85
89
  createCrawlerTool,
86
90
  createResearchWorkflow
87
91
  } from "maqam";
@@ -97,6 +101,15 @@ gateway.registerTool("crawler", createCrawlerTool());
97
101
  gateway.registerTool("summarizer", createAgentTool(async (input) => ({
98
102
  summary: `Reviewed ${input.topic}`
99
103
  }), { name: "summarizer" }));
104
+ gateway.registerTool("localWorker", createCliAgentTool({
105
+ name: "localWorker",
106
+ command: process.execPath,
107
+ args: ["--version"],
108
+ stdin: "none",
109
+ timeoutMs: 5000,
110
+ maxInputTokens: 20,
111
+ maxOutputBytes: 2048
112
+ }));
100
113
 
101
114
  const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway: gateway });
102
115
  const result = await runtime.runWorkflow(
@@ -154,7 +167,7 @@ Brand assets live in `app/assets/`, including `maqam-logo.svg` and `maqam-brand-
154
167
  - Use a clear user agent.
155
168
  - Rate-limit per origin.
156
169
  - Avoid bypassing access controls, paywalls, anti-bot systems, or private content.
157
- - No required AI provider dependency.
170
+ - No required model provider dependency.
158
171
  - No required external hosted service.
159
172
  - Produce JSON/JSONL output that agents can consume directly.
160
173
 
@@ -181,3 +194,7 @@ Publishing requires an authenticated npm session with permission to publish the
181
194
  ## License
182
195
 
183
196
  MIT
197
+
198
+ ## Open Development
199
+
200
+ Maqam is open source under MIT and open for development, issues, ideas, and contributions.
package/docs/usage.md CHANGED
@@ -15,6 +15,7 @@ This guide covers installation, CLI usage, SDK usage, the local console, crawler
15
15
  - [API Reference](#api-reference)
16
16
  - [Build A Custom Workflow](#build-a-custom-workflow)
17
17
  - [Control Any Agent](#control-any-agent)
18
+ - [Control CLI Workers](#control-cli-workers)
18
19
  - [Register A Custom Tool](#register-a-custom-tool)
19
20
  - [Use Policy And Approvals](#use-policy-and-approvals)
20
21
  - [Use Evidence And Claims](#use-evidence-and-claims)
@@ -79,6 +80,7 @@ import {
79
80
  PolicyEngine,
80
81
  ToolGateway,
81
82
  createAgentTool,
83
+ createCliAgentTool,
82
84
  createCrawlerTool,
83
85
  createResearchWorkflow
84
86
  } from "maqam";
@@ -94,6 +96,15 @@ toolGateway.registerTool("crawler", createCrawlerTool());
94
96
  toolGateway.registerTool("summarizer", createAgentTool(async (input) => ({
95
97
  summary: `Reviewed ${input.topic}`
96
98
  }), { name: "summarizer" }));
99
+ toolGateway.registerTool("localWorker", createCliAgentTool({
100
+ name: "localWorker",
101
+ command: process.execPath,
102
+ args: ["--version"],
103
+ stdin: "none",
104
+ timeoutMs: 5000,
105
+ maxInputTokens: 20,
106
+ maxOutputBytes: 2048
107
+ }));
97
108
 
98
109
  const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
99
110
  const result = await runtime.runWorkflow(
@@ -216,6 +227,7 @@ import {
216
227
  ToolGateway,
217
228
  SkillRegistry,
218
229
  createAgentTool,
230
+ createCliAgentTool,
219
231
  createCrawlerTool,
220
232
  createResearchWorkflow,
221
233
  crawl,
@@ -252,6 +264,7 @@ Core objects:
252
264
  - `PolicyEngine`: decides what is allowed, denied, or approval-gated.
253
265
  - `ToolGateway`: routes all external tool calls through policy.
254
266
  - `createAgentTool`: wraps arbitrary agents so they can be governed like any other tool.
267
+ - `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
255
268
  - `EvidenceLedger`: stores source evidence and claim support.
256
269
  - `SkillRegistry`: stores skill metadata and selects matching skills.
257
270
  - `createResearchWorkflow`: bundled workflow for public web research.
@@ -643,6 +656,93 @@ const workflow = createResearchWorkflow({
643
656
  });
644
657
  ```
645
658
 
659
+ ### `createCliAgentTool(options)`
660
+
661
+ Wraps a fixed command-line worker so it can run through Maqam policy and trace capture.
662
+
663
+ ```js
664
+ const localWorker = createCliAgentTool({
665
+ name: "localWorker",
666
+ command: process.execPath,
667
+ args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ artifact: `built:${input.name}` }));"],
668
+ stdin: "json",
669
+ parseJson: true,
670
+ timeoutMs: 5000,
671
+ maxInputTokens: 50,
672
+ maxOutputBytes: 2048
673
+ });
674
+
675
+ toolGateway.registerTool("localWorker", localWorker);
676
+
677
+ const result = await toolGateway.call("localWorker", {
678
+ name: "demo-widget"
679
+ });
680
+
681
+ console.log(result.json.artifact);
682
+ ```
683
+
684
+ Options:
685
+
686
+ | Field | Type | Description |
687
+ | --- | --- | --- |
688
+ | `name` | `string` | Name used in result metadata. |
689
+ | `command` | `string` | Fixed executable path or command. Required. |
690
+ | `args` | `string[]` | Fixed argument list. Dynamic user input should go through stdin, not command args. |
691
+ | `cwd` | `string` | Optional working directory. |
692
+ | `env` | `object` | Extra environment variables. |
693
+ | `inheritEnv` | `boolean` | Inherit `process.env`. Default: `true`. |
694
+ | `stdin` | `"json" | "text" | "none"` | How input is passed to the worker. Default: `"json"`. |
695
+ | `parseJson` | `boolean` | Parse stdout as JSON and expose it as `result.json`. |
696
+ | `timeoutMs` | `number` | Hard runtime timeout. Default: `30000`. |
697
+ | `maxInputTokens` | `number` | Approximate input token limit. Default: `4000`. |
698
+ | `maxOutputBytes` | `number` | Maximum combined stdout/stderr bytes. Default: `65536`. |
699
+ | `rejectOnNonZero` | `boolean` | Reject when exit code is not zero. Default: `true`. |
700
+ | `shell` | `boolean` | Run through a shell. Default: `false`. Use only when a platform wrapper requires it. |
701
+
702
+ Result shape:
703
+
704
+ ```json
705
+ {
706
+ "name": "localWorker",
707
+ "command": "node",
708
+ "args": ["--version"],
709
+ "exitCode": 0,
710
+ "signal": null,
711
+ "timedOut": false,
712
+ "stdout": "v20.0.0\n",
713
+ "stderr": "",
714
+ "durationMs": 42,
715
+ "approxInputTokens": 0,
716
+ "outputBytes": 9,
717
+ "limits": {
718
+ "maxInputTokens": 50,
719
+ "maxOutputBytes": 2048,
720
+ "timeoutMs": 5000
721
+ }
722
+ }
723
+ ```
724
+
725
+ Limit errors:
726
+
727
+ | Code | Meaning |
728
+ | --- | --- |
729
+ | `CLI_INPUT_LIMIT_EXCEEDED` | Input was too large before execution. |
730
+ | `CLI_OUTPUT_LIMIT_EXCEEDED` | stdout/stderr exceeded the configured byte limit. |
731
+ | `CLI_TIMEOUT` | Process exceeded `timeoutMs`. |
732
+ | `CLI_EXIT_NONZERO` | Process exited with a non-zero code. |
733
+ | `CLI_JSON_PARSE_FAILED` | `parseJson` was enabled but stdout was not valid JSON. |
734
+ | `CLI_SPAWN_FAILED` | The process could not be started. |
735
+
736
+ Security notes:
737
+
738
+ - Maqam does not use a shell for CLI workers by default.
739
+ - Keep `command` and `args` fixed in code.
740
+ - Send user input through stdin.
741
+ - Use narrow `allowedTools`.
742
+ - Set short `timeoutMs` and small `maxOutputBytes` for untrusted workers.
743
+ - Use approval gates for workers that write, publish, send, or modify state.
744
+ - Prefer direct executable paths over platform wrapper scripts. On Windows, some `.cmd` or `.ps1` shims may require `shell: true` or a direct underlying executable path.
745
+
646
746
  Tasks:
647
747
 
648
748
  | Task ID | Purpose |
@@ -895,7 +995,7 @@ What Maqam can control:
895
995
 
896
996
  - Function agents.
897
997
  - LangChain/LangGraph-style agents if exposed through `invoke` or wrapped in a function.
898
- - OpenAI Agents SDK-style functions if wrapped in a function.
998
+ - External SDK-style functions if wrapped in a function.
899
999
  - Browser agents.
900
1000
  - Research agents.
901
1001
  - GitHub/npm/internal API agents.
@@ -907,6 +1007,91 @@ What Maqam cannot do automatically:
907
1007
  - It cannot make an unsafe third-party agent safe if that agent bypasses the wrapper and performs side effects internally.
908
1008
  - It cannot approve risky actions by itself; approval-gated actions should be routed to humans.
909
1009
 
1010
+ ## Control CLI Workers
1011
+
1012
+ Maqam can govern command-line workers the same way it governs function agents.
1013
+
1014
+ The pattern is:
1015
+
1016
+ 1. Create a fixed CLI adapter with `createCliAgentTool`.
1017
+ 2. Register it with `ToolGateway`.
1018
+ 3. Add the worker name to `allowedTools`.
1019
+ 4. Configure `timeoutMs`, `maxInputTokens`, and `maxOutputBytes`.
1020
+ 5. Call the worker from a workflow task.
1021
+
1022
+ Example workflow:
1023
+
1024
+ ```js
1025
+ import {
1026
+ AgentRuntime,
1027
+ EvidenceLedger,
1028
+ PolicyEngine,
1029
+ ToolGateway,
1030
+ createCliAgentTool
1031
+ } from "maqam";
1032
+
1033
+ const evidenceLedger = new EvidenceLedger();
1034
+ const policyEngine = new PolicyEngine({
1035
+ allowedTools: ["builderWorker"]
1036
+ });
1037
+ const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
1038
+
1039
+ toolGateway.registerTool("builderWorker", createCliAgentTool({
1040
+ name: "builderWorker",
1041
+ command: process.execPath,
1042
+ args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ fileName: `${input.name}.txt`, content: `Built ${input.name}` }));"],
1043
+ stdin: "json",
1044
+ parseJson: true,
1045
+ timeoutMs: 5000,
1046
+ maxInputTokens: 100,
1047
+ maxOutputBytes: 4096
1048
+ }));
1049
+
1050
+ const workflow = {
1051
+ name: "governed_cli_build",
1052
+ tasks: [
1053
+ {
1054
+ id: "build",
1055
+ run: (context) => context.tools.call("builderWorker", {
1056
+ name: "demo-widget"
1057
+ }, context)
1058
+ },
1059
+ {
1060
+ id: "record",
1061
+ run: (context) => {
1062
+ const output = context.outputs.build.json;
1063
+ const evidence = context.evidence.addEvidence({
1064
+ runId: context.runId,
1065
+ taskId: "record",
1066
+ sourceType: "cli_worker_output",
1067
+ source: "builderWorker",
1068
+ excerpt: output.content,
1069
+ tool: "builderWorker",
1070
+ confidence: 0.8
1071
+ });
1072
+ context.evidence.addClaim({
1073
+ runId: context.runId,
1074
+ taskId: "record",
1075
+ text: `The worker created ${output.fileName}.`,
1076
+ evidenceIds: [evidence.evidenceId],
1077
+ confidence: 0.8
1078
+ });
1079
+ return output;
1080
+ }
1081
+ }
1082
+ ]
1083
+ };
1084
+
1085
+ const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
1086
+ const result = await runtime.runWorkflow(workflow, {
1087
+ objective: "Build a small artifact through a governed CLI worker",
1088
+ allowedTools: ["builderWorker"]
1089
+ });
1090
+
1091
+ console.log(result.outputs.record);
1092
+ console.log(result.evidence.unsupportedClaims);
1093
+ ```
1094
+
910
1095
  ## Register A Custom Tool
911
1096
 
912
1097
  Tools should be small and explicit. The gateway handles policy and trace capture.
@@ -1258,3 +1443,7 @@ Useful next packages or modules:
1258
1443
  - Browser automation connector.
1259
1444
  - GitHub and npm metadata connectors.
1260
1445
  - Tenant-aware configuration and audit export.
1446
+
1447
+ ## Open Development
1448
+
1449
+ Maqam is open source under MIT and open for development, issues, ideas, and contributions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maqam",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Maqam is an MIT-licensed Ajnas agent framework for governed workflows, policy, evidence, skills, and crawler-backed research.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -43,6 +43,8 @@
43
43
  "skills",
44
44
  "tool-orchestration",
45
45
  "human-approval",
46
+ "cli-agent",
47
+ "command-runner",
46
48
  "crawler",
47
49
  "agent",
48
50
  "web-crawler",
@@ -0,0 +1,185 @@
1
+ import { spawn } from "node:child_process";
2
+ import { AjnasFrameworkError } from "./errors.js";
3
+
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const DEFAULT_MAX_INPUT_TOKENS = 4_000;
6
+ const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
7
+
8
+ function estimateTokens(value) {
9
+ return Math.ceil(Buffer.byteLength(value || "", "utf8") / 4);
10
+ }
11
+
12
+ function buildStdin(input, mode) {
13
+ if (mode === "none") return null;
14
+ if (mode === "text") return String(input.prompt ?? input.text ?? "");
15
+ return JSON.stringify(input);
16
+ }
17
+
18
+ function cliError(message, code, details = {}) {
19
+ return new AjnasFrameworkError(message, {
20
+ code,
21
+ details
22
+ });
23
+ }
24
+
25
+ export function createCliAgentTool(options = {}) {
26
+ const {
27
+ name = "cliAgent",
28
+ command,
29
+ args = [],
30
+ cwd,
31
+ env = {},
32
+ inheritEnv = true,
33
+ stdin = "json",
34
+ parseJson = false,
35
+ timeoutMs = DEFAULT_TIMEOUT_MS,
36
+ maxInputTokens = DEFAULT_MAX_INPUT_TOKENS,
37
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
38
+ rejectOnNonZero = true,
39
+ shell = false
40
+ } = options;
41
+
42
+ if (!command || typeof command !== "string") {
43
+ throw new TypeError("createCliAgentTool requires a fixed command string.");
44
+ }
45
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
46
+ throw new TypeError("createCliAgentTool args must be an array of strings.");
47
+ }
48
+
49
+ return async function cliAgentTool(input = {}) {
50
+ const stdinBody = buildStdin(input, stdin);
51
+ const approxInputTokens = estimateTokens(stdinBody || "");
52
+ if (maxInputTokens && approxInputTokens > maxInputTokens) {
53
+ throw cliError(`CLI input exceeds maxInputTokens (${approxInputTokens} > ${maxInputTokens}).`, "CLI_INPUT_LIMIT_EXCEEDED", {
54
+ name,
55
+ approxInputTokens,
56
+ maxInputTokens
57
+ });
58
+ }
59
+
60
+ return new Promise((resolve, reject) => {
61
+ const startedAt = Date.now();
62
+ let child;
63
+ try {
64
+ child = spawn(command, args, {
65
+ cwd,
66
+ env: inheritEnv ? { ...process.env, ...env } : { ...env },
67
+ shell,
68
+ windowsHide: true,
69
+ stdio: ["pipe", "pipe", "pipe"]
70
+ });
71
+ } catch (error) {
72
+ reject(cliError(error.message, "CLI_SPAWN_FAILED", {
73
+ name,
74
+ command,
75
+ cause: error.code || error.name
76
+ }));
77
+ return;
78
+ }
79
+
80
+ const stdout = [];
81
+ const stderr = [];
82
+ let outputBytes = 0;
83
+ let settled = false;
84
+
85
+ const finish = (callback, value) => {
86
+ if (settled) return;
87
+ settled = true;
88
+ clearTimeout(timer);
89
+ callback(value);
90
+ };
91
+
92
+ const stopWithError = (error) => {
93
+ if (!child.killed) child.kill();
94
+ finish(reject, error);
95
+ };
96
+
97
+ const timer = setTimeout(() => {
98
+ stopWithError(cliError(`CLI agent '${name}' timed out after ${timeoutMs}ms.`, "CLI_TIMEOUT", {
99
+ name,
100
+ timeoutMs
101
+ }));
102
+ }, timeoutMs);
103
+
104
+ const collect = (target, chunk) => {
105
+ outputBytes += chunk.byteLength;
106
+ if (outputBytes > maxOutputBytes) {
107
+ stopWithError(cliError(`CLI output exceeds maxOutputBytes (${outputBytes} > ${maxOutputBytes}).`, "CLI_OUTPUT_LIMIT_EXCEEDED", {
108
+ name,
109
+ maxOutputBytes,
110
+ outputBytes
111
+ }));
112
+ return;
113
+ }
114
+ target.push(Buffer.from(chunk));
115
+ };
116
+
117
+ child.stdout.on("data", (chunk) => collect(stdout, chunk));
118
+ child.stderr.on("data", (chunk) => collect(stderr, chunk));
119
+
120
+ child.on("error", (error) => {
121
+ finish(reject, cliError(error.message, "CLI_SPAWN_FAILED", {
122
+ name,
123
+ command,
124
+ cause: error.code || error.name
125
+ }));
126
+ });
127
+
128
+ child.on("close", (exitCode, signal) => {
129
+ if (settled) return;
130
+
131
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
132
+ const stderrText = Buffer.concat(stderr).toString("utf8");
133
+ const result = {
134
+ name,
135
+ command,
136
+ args,
137
+ exitCode,
138
+ signal,
139
+ timedOut: false,
140
+ stdout: stdoutText,
141
+ stderr: stderrText,
142
+ durationMs: Date.now() - startedAt,
143
+ approxInputTokens,
144
+ outputBytes,
145
+ limits: {
146
+ maxInputTokens,
147
+ maxOutputBytes,
148
+ timeoutMs
149
+ }
150
+ };
151
+
152
+ if (parseJson && stdoutText.trim()) {
153
+ try {
154
+ result.json = JSON.parse(stdoutText.trim());
155
+ } catch (error) {
156
+ finish(reject, cliError("CLI stdout was not valid JSON.", "CLI_JSON_PARSE_FAILED", {
157
+ name,
158
+ message: error.message
159
+ }));
160
+ return;
161
+ }
162
+ }
163
+
164
+ if (rejectOnNonZero && exitCode !== 0) {
165
+ finish(reject, cliError(`CLI agent '${name}' exited with code ${exitCode}.`, "CLI_EXIT_NONZERO", {
166
+ ...result,
167
+ stdout: stdoutText.slice(0, 2048),
168
+ stderr: stderrText.slice(0, 2048)
169
+ }));
170
+ return;
171
+ }
172
+
173
+ finish(resolve, result);
174
+ });
175
+
176
+ if (stdinBody === null) {
177
+ child.stdin.end();
178
+ } else {
179
+ child.stdin.end(stdinBody);
180
+ }
181
+ });
182
+ };
183
+ }
184
+
185
+ export { estimateTokens as estimateCliInputTokens };
package/src/index.js CHANGED
@@ -340,6 +340,7 @@ export { ToolGateway } from "./framework/tool-gateway.js";
340
340
  export { SkillRegistry } from "./framework/skill-registry.js";
341
341
  export { AgentRuntime } from "./framework/runtime.js";
342
342
  export { createAgentTool } from "./framework/agent-tool.js";
343
+ export { createCliAgentTool, estimateCliInputTokens } from "./framework/cli-agent-tool.js";
343
344
  export { createResearchWorkflow } from "./framework/research-workflow.js";
344
345
 
345
346
  export function createCrawlerTool(defaultOptions = {}) {