@numa-tech/numa 1.20.0 → 1.20.1

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.
Files changed (102) hide show
  1. package/README.md +28 -2
  2. package/dist/ai-registry/bridge.js +3 -2
  3. package/dist/ai-registry/bridge.js.map +1 -1
  4. package/dist/ai-registry/client.d.ts +8 -1
  5. package/dist/ai-registry/client.js +11 -1
  6. package/dist/ai-registry/client.js.map +1 -1
  7. package/dist/ai-registry/commands.d.ts +3 -0
  8. package/dist/ai-registry/commands.js +48 -36
  9. package/dist/ai-registry/commands.js.map +1 -1
  10. package/dist/ai-registry/mcp-management.d.ts +2 -0
  11. package/dist/ai-registry/mcp-management.js +74 -0
  12. package/dist/ai-registry/mcp-management.js.map +1 -0
  13. package/dist/ai-registry/mcp-runtime-commands.d.ts +15 -0
  14. package/dist/ai-registry/mcp-runtime-commands.js +101 -0
  15. package/dist/ai-registry/mcp-runtime-commands.js.map +1 -0
  16. package/dist/ai-registry/mcp-runtime.d.ts +37 -0
  17. package/dist/ai-registry/mcp-runtime.js +134 -0
  18. package/dist/ai-registry/mcp-runtime.js.map +1 -0
  19. package/dist/ai-registry/mcp-stdio.d.ts +2 -0
  20. package/dist/ai-registry/mcp-stdio.js +18 -0
  21. package/dist/ai-registry/mcp-stdio.js.map +1 -0
  22. package/dist/authorization/application-client.d.ts +1271 -0
  23. package/dist/authorization/application-client.js +391 -0
  24. package/dist/authorization/application-client.js.map +1 -0
  25. package/dist/authorization/application-commands.d.ts +15 -0
  26. package/dist/authorization/application-commands.js +240 -0
  27. package/dist/authorization/application-commands.js.map +1 -0
  28. package/dist/authorization/application-schemas.d.ts +731 -0
  29. package/dist/authorization/application-schemas.js +521 -0
  30. package/dist/authorization/application-schemas.js.map +1 -0
  31. package/dist/authorization/{client.d.ts → bootstrap-client.d.ts} +5 -26
  32. package/dist/authorization/{client.js → bootstrap-client.js} +4 -27
  33. package/dist/authorization/bootstrap-client.js.map +1 -0
  34. package/dist/authorization/commands.d.ts +6 -21
  35. package/dist/authorization/commands.js +8 -130
  36. package/dist/authorization/commands.js.map +1 -1
  37. package/dist/authorization/enrollment-client.d.ts +40 -0
  38. package/dist/authorization/enrollment-client.js +56 -0
  39. package/dist/authorization/enrollment-client.js.map +1 -0
  40. package/dist/authorization/enrollment-commands.d.ts +5 -0
  41. package/dist/authorization/enrollment-commands.js +30 -0
  42. package/dist/authorization/enrollment-commands.js.map +1 -0
  43. package/dist/authorization/enrollment.d.ts +2 -2
  44. package/dist/authorization/enrollment.js +2 -2
  45. package/dist/authorization/enrollment.js.map +1 -1
  46. package/dist/authorization/schemas.d.ts +0 -90
  47. package/dist/authorization/schemas.js +0 -48
  48. package/dist/authorization/schemas.js.map +1 -1
  49. package/dist/backend.d.ts +1 -0
  50. package/dist/backend.js +4 -1
  51. package/dist/backend.js.map +1 -1
  52. package/dist/business-issuer.d.ts +4 -0
  53. package/dist/business-issuer.js +30 -0
  54. package/dist/business-issuer.js.map +1 -0
  55. package/dist/cli.js +61 -30
  56. package/dist/cli.js.map +1 -1
  57. package/dist/cloud/bootstrap.js +1 -1
  58. package/dist/cloud/bootstrap.js.map +1 -1
  59. package/dist/command-catalog.js +11 -36
  60. package/dist/command-catalog.js.map +1 -1
  61. package/dist/config.d.ts +1 -1
  62. package/dist/config.js +15 -6
  63. package/dist/config.js.map +1 -1
  64. package/dist/login-configuration.d.ts +8 -0
  65. package/dist/login-configuration.js +17 -0
  66. package/dist/login-configuration.js.map +1 -0
  67. package/dist/login-recovery.d.ts +2 -0
  68. package/dist/login-recovery.js +15 -0
  69. package/dist/login-recovery.js.map +1 -0
  70. package/dist/mcp/commands.js +12 -2
  71. package/dist/mcp/commands.js.map +1 -1
  72. package/dist/mcp-gateway/commands.d.ts +2 -0
  73. package/dist/mcp-gateway/commands.js +11 -0
  74. package/dist/mcp-gateway/commands.js.map +1 -1
  75. package/dist/oss/commands.js +15 -15
  76. package/dist/oss/commands.js.map +1 -1
  77. package/dist/oss/tools.js +1 -1
  78. package/dist/oss/tools.js.map +1 -1
  79. package/dist/oss/web.js +1 -1
  80. package/dist/oss/web.js.map +1 -1
  81. package/dist/platform-profile.d.ts +22 -2
  82. package/dist/platform-profile.js +67 -6
  83. package/dist/platform-profile.js.map +1 -1
  84. package/docs/application-default-groups.md +73 -0
  85. package/examples/cloud-bootstrap/README.md +1 -1
  86. package/examples/mcp/README.md +108 -0
  87. package/examples/mcp/management-smoke.py +154 -0
  88. package/examples/mcp/runtime-smoke.mjs +187 -0
  89. package/examples/mcp/server.json +17 -0
  90. package/examples/oss/README.md +78 -0
  91. package/examples/oss/__pycache__/roundtrip.cpython-314.pyc +0 -0
  92. package/examples/oss/__pycache__/test_roundtrip.cpython-314.pyc +0 -0
  93. package/examples/oss/roundtrip.py +84 -0
  94. package/examples/oss/test_roundtrip.py +66 -0
  95. package/package.json +5 -4
  96. package/dist/authorization/client.js.map +0 -1
  97. package/dist/authorization/group-client.d.ts +0 -169
  98. package/dist/authorization/group-client.js +0 -129
  99. package/dist/authorization/group-client.js.map +0 -1
  100. package/dist/authorization/group-commands.d.ts +0 -25
  101. package/dist/authorization/group-commands.js +0 -155
  102. package/dist/authorization/group-commands.js.map +0 -1
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // Local MVP acceptance. Identity/profile/cache are supplied through the normal Numa environment.
3
+ // No credentials are printed or written by this runner.
4
+ import assert from "node:assert/strict";
5
+ import { spawn } from "node:child_process";
6
+ import { parseArgs } from "node:util";
7
+ import { readFile } from "node:fs/promises";
8
+ import { resolve } from "node:path";
9
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
+
12
+ const { values } = parseArgs({ options: {
13
+ cli: { type: "string", default: "dist/cli.js" },
14
+ namespace: { type: "string", default: "mcp-lab-a" },
15
+ instance: { type: "string" },
16
+ server: { type: "string", default: "catalog-mcp" },
17
+ tool: { type: "string", default: "search_products" },
18
+ "denied-tool": { type: "string", default: "update_product" },
19
+ "error-tool": { type: "string", default: "search_failure" },
20
+ "reader-env-file": { type: "string" },
21
+ "writer-env-file": { type: "string" },
22
+ "denied-env-file": { type: "string" },
23
+ "other-namespace": { type: "string", default: "mcp-lab-b" },
24
+ extended: { type: "boolean", default: false }
25
+ } });
26
+ async function identityEnvironment(path) {
27
+ const provided = path ? JSON.parse(await readFile(path, "utf8")) : {};
28
+ if (!provided || typeof provided !== "object" || Array.isArray(provided)
29
+ || Object.values(provided).some(value => typeof value !== "string")) throw new Error("Identity environment must be a string map.");
30
+ const environment = { ...process.env, ...provided };
31
+ const base = environment.NUMA_BACKEND_BASE_URL;
32
+ const parsed = base ? new URL(base) : undefined;
33
+ if (!parsed || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash
34
+ || !["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) {
35
+ throw new Error("This runner requires NUMA_BACKEND_BASE_URL to explicitly select the local MVP.");
36
+ }
37
+ return environment;
38
+ }
39
+ const readerEnvironment = await identityEnvironment(values["reader-env-file"]);
40
+ const writerEnvironment = values["writer-env-file"] ? await identityEnvironment(values["writer-env-file"]) : undefined;
41
+ const deniedEnvironment = values["denied-env-file"] ? await identityEnvironment(values["denied-env-file"]) : undefined;
42
+ const cli = resolve(values.cli);
43
+ const namespaceOptions = (name = values.namespace) => ["--namespace", name, ...(values.instance ? ["--instance", values.instance] : [])];
44
+ const checks = [];
45
+ let lastOperation;
46
+ async function invoke(args, environment = readerEnvironment, targetNamespace = values.namespace) {
47
+ const child = spawn(process.execPath, [cli, ...args, ...namespaceOptions(targetNamespace), "--json"], {
48
+ env: environment, stdio: ["ignore", "pipe", "pipe"]
49
+ });
50
+ let stdout = "";
51
+ child.stdout.on("data", chunk => { stdout += chunk; });
52
+ child.stderr.resume();
53
+ const timer = setTimeout(() => child.kill("SIGTERM"), 45_000);
54
+ const code = await new Promise((resolveCode, reject) => { child.once("error", reject); child.once("close", resolveCode); });
55
+ clearTimeout(timer);
56
+ let body;
57
+ try { body = JSON.parse(stdout); } catch { throw new Error("CLI did not return valid JSON; inspect local diagnostics."); }
58
+ lastOperation = { command: args.slice(0, 4).join(" "), namespace: targetNamespace, exitCode: code,
59
+ ok: body.ok, errorCode: body.error?.code, httpStatus: body.error?.details?.status ?? body.error?.status };
60
+ return { code, body };
61
+ }
62
+ async function viaProxy(operation, environment = readerEnvironment, server = values.server, targetNamespace = values.namespace) {
63
+ lastOperation = { command: "mcp proxy", server, namespace: targetNamespace };
64
+ const transport = new StdioClientTransport({ command: process.execPath,
65
+ args: [cli, "mcp", "proxy", server, ...namespaceOptions(targetNamespace)],
66
+ env: Object.fromEntries(Object.entries(environment).filter(([, value]) => value !== undefined)), stderr: "pipe" });
67
+ transport.stderr?.resume();
68
+ const client = new Client({ name: "numa-local-mvp-acceptance", version: "1" }, { capabilities: {} });
69
+ try { await client.connect(transport, { timeout: 30_000 }); return await operation(client); }
70
+ finally { await client.close(); }
71
+ }
72
+ try {
73
+ const tools = await invoke(["mcp", "tools", values.server]);
74
+ assert.equal(tools.code, 0);
75
+ assert.equal(tools.body.ok, true);
76
+ const names = tools.body.tools.map(tool => tool.name);
77
+ assert(names.includes(values.tool));
78
+ assert(!names.includes(values["denied-tool"]));
79
+ checks.push("CLI tools uses gateway-filtered list");
80
+
81
+ const call = await invoke(["mcp", "call", values.server, values.tool, "--args", "{}"]);
82
+ assert.equal(call.code, 0);
83
+ assert.equal(call.body.ok, true);
84
+ assert(call.body.result.content?.length > 0);
85
+ checks.push("CLI tool call completes through gateway and upstream");
86
+
87
+ const forbidden = await invoke(["mcp", "call", values.server, values["denied-tool"], "--args", "{}"]);
88
+ assert.notEqual(forbidden.code, 0);
89
+ assert.equal(forbidden.body.ok, false);
90
+ assert.equal(forbidden.body.error.code, "ai_registry_mcp_forbidden");
91
+ checks.push("CLI forbidden tool is denied by gateway");
92
+
93
+ const error = await invoke(["mcp", "call", values.server, values["error-tool"], "--args", "{}"]);
94
+ assert.notEqual(error.code, 0);
95
+ assert.equal(error.body.ok, false);
96
+ assert.equal(error.body.result.isError, true);
97
+ checks.push("CLI preserves executed tool isError and exits nonzero");
98
+
99
+ await viaProxy(async client => {
100
+ assert(client.getServerCapabilities()?.tools);
101
+ const listed = await client.listTools();
102
+ const proxyNames = listed.tools.map(tool => tool.name);
103
+ assert(proxyNames.includes(values.tool));
104
+ assert(!proxyNames.includes(values["denied-tool"]));
105
+ checks.push("stdio proxy negotiates real server and retains filtered tools");
106
+ const result = await client.callTool({ name: values.tool, arguments: {} });
107
+ assert(!result.isError);
108
+ assert(result.content?.length > 0);
109
+ checks.push("stdio MCP client invokes real remote tool via CLI proxy");
110
+ const failed = await client.callTool({ name: values["error-tool"], arguments: {} });
111
+ assert.equal(failed.isError, true);
112
+ checks.push("stdio proxy preserves protocol tool errors");
113
+ });
114
+ let denied = false; let initialized = false;
115
+ try {
116
+ await viaProxy(client => { initialized = true; return client.callTool({ name: values["denied-tool"], arguments: {} }, undefined, { timeout: 15_000 }); });
117
+ } catch { denied = true; }
118
+ assert(initialized && denied);
119
+ checks.push("stdio proxy cannot bypass gateway tool authorization");
120
+ if (values.extended) {
121
+ for (const [server, tool, args, expected] of [
122
+ ["notes-mcp", "list_notes", {}, "credentialVerified"],
123
+ ["stdio-mcp", "calculate_total", { unit_price: 12.5, quantity: 2 }, '"total":25']
124
+ ]) {
125
+ const call = await invoke(["mcp", "call", server, tool, "--args", JSON.stringify(args)]);
126
+ assert.equal(call.code, 0);
127
+ assert.equal(call.body.ok, true);
128
+ const serialized = JSON.stringify(call.body.result).replaceAll(/\s/gu, "").replaceAll('\\"', '"');
129
+ assert(serialized.includes(expected));
130
+ checks.push(`CLI reaches ${server} real upstream adapter`);
131
+ await viaProxy(async client => {
132
+ const tools = await client.listTools();
133
+ assert(tools.tools.some(item => item.name === tool));
134
+ const result = await client.callTool({ name: tool, arguments: args });
135
+ assert(!result.isError);
136
+ const serialized = JSON.stringify(result).replaceAll(/\s/gu, "").replaceAll('\\"', '"');
137
+ assert(serialized.includes(expected));
138
+ }, readerEnvironment, server);
139
+ checks.push(`local stdio proxy reaches ${server} real upstream adapter`);
140
+ }
141
+ await viaProxy(async client => {
142
+ const first = await client.listTools();
143
+ const second = await client.callTool({ name: values.tool, arguments: {} });
144
+ assert(first.tools.some(tool => tool.name === values.tool));
145
+ assert(!second.isError);
146
+ }, readerEnvironment, "session-mcp");
147
+ checks.push("stdio proxy retains stateful upstream session across requests");
148
+ }
149
+ if (writerEnvironment) {
150
+ const write = await invoke(["mcp", "call", values.server, values["denied-tool"], "--args", '{"name":"numa-local-acceptance"}'], writerEnvironment);
151
+ assert.equal(write.code, 0);
152
+ assert.equal(write.body.ok, true);
153
+ checks.push("writer CLI executes authorized fixture write");
154
+ await viaProxy(async client => {
155
+ const tools = await client.listTools();
156
+ assert(tools.tools.some(tool => tool.name === values["denied-tool"]));
157
+ const written = await client.callTool({ name: values["denied-tool"], arguments: { name: "numa-stdio-acceptance" } });
158
+ assert(!written.isError);
159
+ }, writerEnvironment);
160
+ checks.push("writer stdio proxy executes authorized fixture write");
161
+ const unclassified = await invoke(["mcp", "call", values.server, "unclassified_operation", "--args", "{}"], writerEnvironment);
162
+ assert.notEqual(unclassified.code, 0);
163
+ assert.equal(unclassified.body.error.code, "ai_registry_mcp_forbidden");
164
+ checks.push("writer still cannot invoke unclassified tool");
165
+ const readOther = await invoke(["mcp", "call", values.server, values.tool, "--args", "{}"], readerEnvironment, values["other-namespace"]);
166
+ assert.notEqual(readOther.code, 0);
167
+ assert.equal(readOther.body.ok, false);
168
+ assert.match(readOther.body.error.code, /forbidden/u);
169
+ checks.push("reader cannot cross into another namespace");
170
+ const writerOther = await invoke(["mcp", "call", values.server, values.tool, "--args", "{}"], writerEnvironment, values["other-namespace"]);
171
+ assert.equal(writerOther.code, 0);
172
+ assert.equal(writerOther.body.ok, true);
173
+ checks.push("explicitly granted writer can call another namespace");
174
+ }
175
+ if (deniedEnvironment) {
176
+ const denied = await invoke(["mcp", "tools", values.server], deniedEnvironment);
177
+ assert.notEqual(denied.code, 0);
178
+ assert.equal(denied.body.ok, false);
179
+ assert(denied.body.error.details?.status === 403 || /forbidden/u.test(denied.body.error.code));
180
+ checks.push("ungranted identity cannot discover callable server");
181
+ }
182
+ process.stdout.write(JSON.stringify({ ok: true, checks }, null, 2) + "\n");
183
+ } catch {
184
+ process.stderr.write(JSON.stringify({ ok: false, completedChecks: checks, lastOperation,
185
+ message: "MCP runtime acceptance failed; credential-bearing responses were withheld." }, null, 2) + "\n");
186
+ process.exitCode = 1;
187
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "serverSpecification": {
3
+ "name": "cli-local-example",
4
+ "description": "Local CLI registration example; gateway access requires a separate binding and authorization.",
5
+ "protocol": "mcp-streamable",
6
+ "frontProtocol": "mcp-streamable",
7
+ "enabled": false,
8
+ "versionDetail": { "version": "1.0.0" },
9
+ "remoteServerConfig": { "exportPath": "/mcp" }
10
+ },
11
+ "toolSpecification": {
12
+ "tools": [{ "name": "echo", "description": "Example metadata only", "inputSchema": { "type": "object", "properties": {} } }],
13
+ "toolsMeta": {}
14
+ },
15
+ "endpointSpecification": { "type": "DIRECT", "data": { "address": "127.0.0.1", "port": "8080" } },
16
+ "latest": true
17
+ }
@@ -0,0 +1,78 @@
1
+ # OSS CLI list / write / read example
2
+
3
+ Use the local build only. Nothing here publishes to npm or changes a global installation.
4
+
5
+ ```bash
6
+ cd /Users/changhz/source/numa-business-issuers
7
+ npm run build
8
+ # A local binary still reads the global configuration unless this is set.
9
+ export NUMA_CONFIG_DIR="$PWD/tmp/issuer-mvp-cli"
10
+ export NUMA_PROFILE_URL='http://localhost:28080/public/v1/client-configurations/issuer-mvp'
11
+ node dist/cli.js config --set profileUrl="$NUMA_PROFILE_URL" profileId=issuer-mvp apiBaseUrl=http://localhost:28080 checkUpdates=false
12
+ node dist/cli.js login --no-console --no-config
13
+ node dist/cli.js oss grants --json
14
+ # Replace 123 with the exact ACTIVE, synced/verified read/write grant shown above.
15
+ python3 examples/oss/roundtrip.py --grant-id 123
16
+ ```
17
+
18
+ For interactive commands, `./scripts/mvp-cli.sh login --issuer cluster-b
19
+ --no-console --no-config` and `./scripts/mvp-cli.sh oss grants --issuer cluster-b`
20
+ set up the same isolated local directory automatically. They never change
21
+ `~/.config/numa`. Merely setting NUMA_PROFILE_URL with your global configuration
22
+ can still leave its pinned profileId pointing at numa-realm.
23
+
24
+ The local profile must first be created and published in DevOps. Its platform
25
+ issuer is fixed; `login` uses the business default. To explicitly select another
26
+ business issuer, use the same selector for login and the example:
27
+
28
+ ```bash
29
+ node dist/cli.js login --issuer cluster-b --no-console --no-config
30
+ node dist/cli.js oss grants --issuer cluster-b --json
31
+ python3 examples/oss/roundtrip.py --issuer cluster-b --grant-id 123
32
+ ```
33
+
34
+ `--cli /absolute/path/dist/cli.js` selects another local build. The script never
35
+ runs npx, downloads packages, chooses a grant automatically or changes permissions.
36
+ It queries Grant/STS APIs through Numa, uses the STS-backed OSS client to list up
37
+ to ten objects, uploads a small unique object, checks metadata, downloads it and
38
+ compares bytes. Both read and write permission are required. `--prefix` must be
39
+ inside the selected grant's scope. No existing object is overwritten. The remote
40
+ test object is retained and its key is printed; temporary local files are removed.
41
+ Credentials are handled internally by the CLI, never printed by the script.
42
+
43
+ Machine identities use the selected issuer too. Configure
44
+ `NUMA_KEYCLOAK_AUTH_MODE=client_credentials`, the dedicated
45
+ `NUMA_KEYCLOAK_CLIENT_ID`, and `NUMA_KEYCLOAK_CLIENT_SECRET_FILE` (owner-only file).
46
+ Do not run browser login in machine mode. The backend still requires the canonical
47
+ OSS role and a grant for this exact issuer/subject.
48
+
49
+ For the local acceptance run, the published `issuer-mvp` Profile defaults to
50
+ cluster-a. Its existing fixture grants are intentionally unpublished/read-only
51
+ and cannot run the write example. Select a separately verified read/write grant
52
+ before executing the example against real OSS.
53
+
54
+ `oss grants` lists authorization records, including records whose cloud policy is
55
+ not yet synced/verified. Its count is not a count of grants ready for object access.
56
+ The normal output shows each grant ID, bucket/prefix, permission and sync state.
57
+ For example, the local MVP fixture Grant 2 has `cloudPolicySynced=false` and
58
+ `permissionLevel=READ_ONLY`: it cannot run this roundtrip. The unified API returns
59
+ `actions` and `effect`; the CLI authorizes operations from those fields, never
60
+ from the template label. `oss ls` only requires Allow + `oss:ListObjects`; stat/get
61
+ require `oss:GetObject`, and upload requires `oss:PutObject`. The roundtrip requires
62
+ all three Actions. System ADMIN uses `oss:*`, which is broader than selecting all
63
+ currently editable object Actions.
64
+ The roundtrip reports all status/permission blockers before requesting STS or
65
+ performing any object operations; it never publishes policies or expands access.
66
+
67
+ If `oss ls` fails with `OSS_STS_BROKER_NOT_CONFIGURED` (HTTP 503), the platform
68
+ account lacks an active STS broker RAM-user AK. A synced OSS policy alone does
69
+ not make STS ready. A platform administrator must configure the dedicated broker
70
+ identity and its managed AK, permission to assume the selected RAM Role, and the
71
+ Role's trust policy. The default management AK is separate from the broker AK.
72
+ `OSS_STS_ASSUME_ROLE_FAILED` means the STS call failed after broker selection;
73
+ check broker credentials, trust, permissions and connectivity. Neither error is
74
+ fixed by logging in with another issuer or changing the grant's read/write level.
75
+
76
+ The imported OSS development branch had a hard-coded machine test bucket. This
77
+ build uses server-authorized Grant/STS scope for both user and machine identities;
78
+ SDK prefix and permission checks remain enabled.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Exercise Grant/STS APIs and OSS list/write/read using an unpublished local Numa build."""
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import pathlib
7
+ import subprocess
8
+ import tempfile
9
+ import uuid
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description=__doc__)
14
+ parser.add_argument('--cli', type=pathlib.Path, default=pathlib.Path(__file__).resolve().parents[2] / 'dist/cli.js', help='Local dist/cli.js; never downloads or installs a CLI')
15
+ parser.add_argument('--issuer', help='Business issuer code from the selected Profile; omit to use its default')
16
+ parser.add_argument('--grant-id', required=True, type=int, help='An exact grant ID from numa oss grants')
17
+ parser.add_argument('--prefix', help='Optional test prefix inside the grant scope')
18
+ args = parser.parse_args()
19
+ if not args.cli.is_file():
20
+ parser.error('Build the local CLI first (npm run build), or pass --cli /absolute/path/dist/cli.js')
21
+ command = ['node', str(args.cli.resolve())]
22
+ if args.issuer:
23
+ command += ['--issuer', args.issuer]
24
+
25
+ def cli(*arguments):
26
+ result = subprocess.run(command + list(arguments) + ['--json'], capture_output=True, text=True)
27
+ try:
28
+ value = json.loads(result.stdout)
29
+ except ValueError:
30
+ raise RuntimeError('CLI did not return JSON; check the local build and Profile configuration') from None
31
+ if result.returncode or not value.get('ok'):
32
+ # Do not echo arbitrary stdout, stderr, API responses or credentials.
33
+ error = value.get('error', {})
34
+ code = error.get('code', 'command_failed') if isinstance(error, dict) else 'command_failed'
35
+ raise RuntimeError(f'CLI command {arguments[0]} {arguments[1]} failed: {code}')
36
+ return value
37
+
38
+ grants = cli('oss', 'grants')['grants']
39
+ grant = next((g for g in grants if g['grantId'] == args.grant_id), None)
40
+ if not grant:
41
+ parser.error(f'Grant {args.grant_id} 未出现在当前身份的授权列表中;请使用相同 --issuer 执行 oss grants 查看授权 ID')
42
+ blockers = []
43
+ if grant['status'] != 'ACTIVE':
44
+ blockers.append('授权未启用(status=' + json.dumps(grant['status'], ensure_ascii=False) + ')')
45
+ if not grant['cloudPolicySynced']:
46
+ blockers.append('云端策略未同步/验证(cloudPolicySynced=false)')
47
+ required_actions = {'oss:ListObjects', 'oss:GetObject', 'oss:PutObject'}
48
+ actions = grant.get('actions')
49
+ permitted = grant.get('effect') == 'Allow' and isinstance(actions, list) and (required_actions.issubset(set(actions)) or 'oss:*' in actions)
50
+ if not permitted:
51
+ blockers.append('读写示例需要 Allow 且同时包含 ListObjects、GetObject、PutObject,当前 actions=' + json.dumps(actions, ensure_ascii=False))
52
+ if blockers:
53
+ parser.error(f'Grant {args.grant_id} 无法运行读写示例:' + ';'.join(blockers) + '。尚未申请 STS 或执行 OSS 对象操作。请选择已同步/验证且具有读写权限的授权。')
54
+ scope = grant['scopeType']
55
+ allowed = grant.get('objectPrefix') or ''
56
+ if scope not in ('BUCKET', 'PREFIX') or scope == 'PREFIX' and not allowed:
57
+ parser.error('Grant scope is incomplete')
58
+ prefix = args.prefix if args.prefix is not None else allowed
59
+ if scope == 'PREFIX' and not prefix.startswith(allowed):
60
+ parser.error('--prefix must be inside the granted objectPrefix')
61
+ if prefix and not prefix.endswith('/'):
62
+ prefix += '/'
63
+ key = prefix + 'numa-cli-example-' + uuid.uuid4().hex + '.txt'
64
+ grant_id = str(args.grant_id)
65
+ print(json.dumps({'bucket': grant['bucketName'], 'object_key': key, 'issuer': args.issuer or 'profile-default'}, ensure_ascii=False))
66
+ cli('oss', 'ls', grant_id, prefix, '--limit', '10')
67
+ with tempfile.TemporaryDirectory(prefix='numa-oss-example-') as directory:
68
+ source = pathlib.Path(directory) / 'source.txt'
69
+ downloaded = pathlib.Path(directory) / 'downloaded.txt'
70
+ source.write_text('Numa OSS CLI roundtrip\n' + str(uuid.uuid4()) + '\n', encoding='utf-8')
71
+ cli('oss', 'put', grant_id, key, str(source)) # No --overwrite.
72
+ cli('oss', 'stat', grant_id, key)
73
+ cli('oss', 'get', grant_id, key, '--output', str(downloaded))
74
+ if source.read_bytes() != downloaded.read_bytes():
75
+ raise RuntimeError('Downloaded contents do not match the uploaded file')
76
+ print(json.dumps({'ok': True, 'verified': ['list', 'write', 'read'], 'sha256': hashlib.sha256(downloaded.read_bytes()).hexdigest(), 'retained_object_key': key}))
77
+ # Deliberately retain the unique remote object. There is no implicit delete.
78
+
79
+
80
+ if __name__ == '__main__':
81
+ try:
82
+ main()
83
+ except (RuntimeError, OSError) as error:
84
+ raise SystemExit(str(error)) from None
@@ -0,0 +1,66 @@
1
+ """Contract tests for the runnable example; all CLI/cloud responses are simulated."""
2
+ import contextlib
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+ from unittest.mock import patch
12
+
13
+ spec=importlib.util.spec_from_file_location('roundtrip',Path(__file__).with_name('roundtrip.py'))
14
+ module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
15
+
16
+ class RoundtripTest(unittest.TestCase):
17
+ def exercise(self,permission='READ_WRITE',prefix=None,corrupt=False,synced=True,status='ACTIVE',missing=False,blocked_message=None):
18
+ calls=[];storage={}
19
+ with tempfile.TemporaryDirectory() as directory:
20
+ entry=Path(directory)/'cli.js';entry.touch()
21
+ argv=['roundtrip','--cli',str(entry),'--grant-id','7','--issuer','cluster-b']
22
+ if prefix is not None:argv += ['--prefix',prefix]
23
+ def run(command,**kwargs):
24
+ self.assertEqual(command[2:4],['--issuer','cluster-b'])
25
+ args=command[4:-1];calls.append(args)
26
+ self.assertEqual(args[0],'oss');self.assertNotIn('--overwrite',args)
27
+ if args[1]=='grants': payload={'grants':[] if missing else [{'grantId':7,'status':status,'cloudPolicySynced':synced,'permissionLevel':permission,'effect':'Allow','actions':['oss:ListObjects','oss:GetObject']+([] if permission=='READ_ONLY' else ['oss:PutObject']),'scopeType':'PREFIX','objectPrefix':'tenant/kb/','bucketName':'example-bucket'}]}
28
+ elif args[1]=='put': storage[args[3]]=Path(args[4]).read_bytes();payload={}
29
+ elif args[1]=='get': Path(args[5]).write_bytes(b'wrong' if corrupt else storage[args[3]]);payload={}
30
+ elif args[1] in ('ls','stat'): payload={}
31
+ else:raise AssertionError('Unexpected object mutation: '+args[1])
32
+ return subprocess.CompletedProcess(command,0,json.dumps({'ok':True,**payload}),'')
33
+ output=io.StringIO();errors=io.StringIO()
34
+ with patch.object(sys,'argv',argv),patch.object(module.subprocess,'run',run),contextlib.redirect_stdout(output),contextlib.redirect_stderr(errors):
35
+ if blocked_message is None:
36
+ module.main()
37
+ else:
38
+ with self.assertRaises(SystemExit) as error:module.main()
39
+ self.assertEqual(error.exception.code,2)
40
+ self.assertRegex(errors.getvalue(),blocked_message)
41
+ self.assertEqual([c[1] for c in calls],['grants'])
42
+ self.assertEqual(storage,{})
43
+ return calls,storage,output.getvalue()
44
+
45
+ def test_roundtrip_selects_one_issuer_and_keeps_unique_object(self):
46
+ calls,storage,output=self.exercise()
47
+ self.assertEqual([c[1] for c in calls],['grants','ls','put','stat','get'])
48
+ key=next(iter(storage));self.assertTrue(key.startswith('tenant/kb/numa-cli-example-'))
49
+ self.assertEqual(json.loads(output.splitlines()[-1])['retained_object_key'],key)
50
+
51
+ def test_refuses_readonly_and_out_of_scope_before_write(self):
52
+ with self.assertRaises(SystemExit):self.exercise(permission='READ_ONLY')
53
+ with self.assertRaises(SystemExit):self.exercise(prefix='another-tenant/')
54
+
55
+ def test_detects_corrupted_download(self):
56
+ with self.assertRaisesRegex(RuntimeError,'do not match'):self.exercise(corrupt=True)
57
+
58
+ def test_reports_all_fixture_blockers_without_requesting_sts_or_objects(self):
59
+ self.exercise(permission='READ_ONLY',synced=False,blocked_message=r'Grant 7.*cloudPolicySynced=false.*actions=.*oss:GetObject.*尚未申请 STS')
60
+
61
+ def test_distinguishes_missing_inactive_and_synced_readonly_grants(self):
62
+ self.exercise(missing=True,blocked_message=r'Grant 7 未出现在当前身份')
63
+ self.exercise(status='DISABLED',blocked_message=r'授权未启用.*DISABLED')
64
+ self.exercise(permission='READ_ONLY',blocked_message=r'需要 Allow.*ListObjects.*GetObject.*PutObject.*actions=')
65
+
66
+ if __name__=='__main__':unittest.main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@numa-tech/numa",
3
- "version": "1.20.0",
3
+ "version": "1.20.1",
4
4
  "description": "Cross-platform CLI for the Numa internal developer platform with Keycloak authentication and MCP support.",
5
5
  "author": "numa-tech",
6
6
  "keywords": [
@@ -22,6 +22,7 @@
22
22
  "dist",
23
23
  "README.md",
24
24
  "docs/wecom-messaging.md",
25
+ "docs/application-default-groups.md",
25
26
  "docs/module-maintenance-and-skills-updates.md",
26
27
  "examples",
27
28
  "skills",
@@ -37,7 +38,7 @@
37
38
  "dev:oss-web": "tsx src/cli.ts oss web",
38
39
  "test:oss-web": "node --import tsx --test src/oss/web.test.ts src/oss/web-session.test.ts src/oss/web-transfers.test.ts src/oss/web-command.test.ts src/oss/local-picker.test.ts src/oss/transfer-paths.test.ts src/oss/transfer-worker.test.ts src/oss/transfer-queue.test.ts src/oss/web-page.test.ts src/oss/file-operations.test.ts",
39
40
  "test:oss-auth-code": "node scripts/test-oss-auth-code.mjs",
40
- "root:test": "node --import tsx --test --test-concurrency=4 src/mcp-gateway/profile.test.ts src/mcp-gateway/runtime.test.ts src/mcp-gateway/install.test.ts src/branding.test.ts src/app-config.test.ts src/platform-profile.test.ts src/identity.test.ts src/backend.test.ts src/oauth.test.ts src/web-console.test.ts src/cli-options.test.ts src/mail/client.test.ts src/wecom/client.test.ts src/wecom/commands.test.ts src/wecom/tools.test.ts src/ai-registry/client.test.ts src/ai-registry/bootstrap.test.ts src/ai-registry/install-defaults.test.ts src/ai-registry/workspace.test.ts src/ai-registry/namespace.test.ts src/ai-registry/sync.test.ts src/ai-registry/bridge.test.ts src/ai-registry/skill-installer.test.ts src/ai-registry/skill-uninstaller.test.ts src/ai-registry/skill-dependencies.test.ts src/ai-registry/updates.test.ts src/maintenance/scheduler.test.ts src/maintenance/codex.test.ts src/ai-registry/mcp-installer.test.ts src/media/client.test.ts src/media/commands.test.ts src/media/transfers.test.ts src/oss/client.test.ts src/oss/commands.test.ts src/oss/auth-code.test.ts src/oss/clipboard.test.ts src/oss/web.test.ts src/oss/web-session.test.ts src/oss/web-transfers.test.ts src/oss/web-command.test.ts src/oss/local-picker.test.ts src/oss/transfer-paths.test.ts src/oss/transfer-worker.test.ts src/oss/transfer-queue.test.ts src/oss/web-page.test.ts src/oss/file-operations.test.ts src/oss/bootstrap.test.ts src/oss/transfers.test.ts src/oss/search.test.ts src/oss/tools.test.ts src/mcp/install.test.ts src/mcp/catalog.test.ts src/interactive/wizard.test.ts src/pipeline/client.test.ts src/pipeline/commands.test.ts src/gitops/client.test.ts src/gitops/commands.test.ts src/repositories/client.test.ts src/repositories/commands.test.ts src/publications/scanner.test.ts src/publications/client.test.ts src/publications/commands.test.ts src/jenkins-jobs/client.test.ts src/jenkins-jobs/commands.test.ts src/clusters/client.test.ts src/clusters/commands.test.ts src/clusters/kubeconfig-discovery.test.ts src/teams/client.test.ts src/teams/commands.test.ts src/authorization/client.test.ts src/authorization/commands.test.ts src/authorization/access-request-client.test.ts src/authorization/access-request-commands.test.ts src/application-candidates/client.test.ts src/application-candidates/commands.test.ts src/application-onboarding/client.test.ts src/application-onboarding/commands.test.ts src/application-onboarding/tui.test.ts src/chatgpt-desktop.test.ts src/codex-integration.test.ts scripts/release-all.test.mjs src/authorization/enrollment.test.ts src/authorization/group-client.test.ts src/authorization/group-commands.test.ts src/notion/client.test.ts src/notion/commands.test.ts src/notion/tools.test.ts src/notion/machine-cli.test.ts src/cloud/accounts.test.ts src/cloud/bootstrap.test.ts src/cloud/business-bootstrap.test.ts src/cloud/maintenance.test.ts src/cloud/wizard.test.ts src/cloud/oss-bootstrap.test.ts",
41
+ "root:test": "node --import tsx --test --test-concurrency=4 src/mcp-gateway/profile.test.ts src/mcp-gateway/runtime.test.ts src/mcp-gateway/install.test.ts src/branding.test.ts src/app-config.test.ts src/platform-profile.test.ts src/identity.test.ts src/backend.test.ts src/oauth.test.ts src/web-console.test.ts src/cli-options.test.ts src/mail/client.test.ts src/wecom/client.test.ts src/wecom/commands.test.ts src/wecom/tools.test.ts src/ai-registry/client.test.ts src/ai-registry/bootstrap.test.ts src/ai-registry/install-defaults.test.ts src/ai-registry/workspace.test.ts src/ai-registry/namespace.test.ts src/ai-registry/sync.test.ts src/ai-registry/bridge.test.ts src/ai-registry/skill-installer.test.ts src/ai-registry/skill-uninstaller.test.ts src/ai-registry/skill-dependencies.test.ts src/ai-registry/updates.test.ts src/maintenance/scheduler.test.ts src/maintenance/codex.test.ts src/ai-registry/mcp-installer.test.ts src/media/client.test.ts src/media/commands.test.ts src/media/transfers.test.ts src/oss/client.test.ts src/oss/commands.test.ts src/oss/auth-code.test.ts src/oss/clipboard.test.ts src/oss/web.test.ts src/oss/web-session.test.ts src/oss/web-transfers.test.ts src/oss/web-command.test.ts src/oss/local-picker.test.ts src/oss/transfer-paths.test.ts src/oss/transfer-worker.test.ts src/oss/transfer-queue.test.ts src/oss/web-page.test.ts src/oss/file-operations.test.ts src/oss/bootstrap.test.ts src/oss/transfers.test.ts src/oss/search.test.ts src/oss/tools.test.ts src/mcp/install.test.ts src/mcp/catalog.test.ts src/interactive/wizard.test.ts src/pipeline/client.test.ts src/pipeline/commands.test.ts src/gitops/client.test.ts src/gitops/commands.test.ts src/repositories/client.test.ts src/repositories/commands.test.ts src/publications/scanner.test.ts src/publications/client.test.ts src/publications/commands.test.ts src/jenkins-jobs/client.test.ts src/jenkins-jobs/commands.test.ts src/clusters/client.test.ts src/clusters/commands.test.ts src/clusters/kubeconfig-discovery.test.ts src/teams/client.test.ts src/teams/commands.test.ts src/authorization/bootstrap-client.test.ts src/authorization/commands.test.ts src/authorization/access-request-client.test.ts src/authorization/access-request-commands.test.ts src/application-candidates/client.test.ts src/application-candidates/commands.test.ts src/application-onboarding/client.test.ts src/application-onboarding/commands.test.ts src/application-onboarding/tui.test.ts src/chatgpt-desktop.test.ts src/codex-integration.test.ts scripts/release-all.test.mjs src/authorization/enrollment.test.ts src/authorization/application-commands.test.ts src/authorization/application-client.test.ts src/authorization/application-schemas.test.ts src/authorization/enrollment-client.test.ts src/notion/client.test.ts src/notion/commands.test.ts src/notion/tools.test.ts src/notion/machine-cli.test.ts src/cloud/accounts.test.ts src/cloud/bootstrap.test.ts src/cloud/business-bootstrap.test.ts src/cloud/maintenance.test.ts src/cloud/wizard.test.ts src/cloud/oss-bootstrap.test.ts src/business-issuer.test.ts src/login-configuration.test.ts src/login-recovery.test.ts src/ai-registry/mcp-management.test.ts src/ai-registry/mcp-runtime.test.ts src/ai-registry/mcp-stdio.test.ts",
41
42
  "pack:check": "turbo run pack:check root:pack:check",
42
43
  "root:pack:check": "node scripts/check-root-package.mjs",
43
44
  "boundaries": "node scripts/check-workspace-boundaries.mjs",
@@ -60,8 +61,8 @@
60
61
  "dependencies": {
61
62
  "@clack/prompts": "^1.7.0",
62
63
  "@modelcontextprotocol/sdk": "^1.29.0",
63
- "@numa-tech/cli-auth": "1.20.0",
64
- "@tokensrc/codex": "1.20.0",
64
+ "@numa-tech/cli-auth": "1.20.1",
65
+ "@tokensrc/codex": "1.20.1",
65
66
  "ali-oss": "^6.23.0",
66
67
  "commander": "^14.0.3",
67
68
  "ink": "^6.8.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/authorization/client.ts"],"names":[],"mappings":"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"}