@cubicecho/agent-core 4.1.0 → 4.2.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/README.md CHANGED
@@ -1165,6 +1165,24 @@ applies `resolveApiKey` afterwards, so a document naming somebody else's endpoin
1165
1165
  host send its own provider key there. `exportSpec(spec)` strips a bundled server's `env` and
1166
1166
  `headers`, and a redacted document is still a valid one.
1167
1167
 
1168
+ Those two fields are where the format puts a bundled server's credentials, and not the only place a
1169
+ pasted `.mcp.json` has them: an `--api-key` among the `args`, a `?token=` or a `user:pass@` in the
1170
+ `url`. `exportSpec` keeps those, since a server with its query string cut off is a different
1171
+ server, and `findSpecSecrets` says where they may be so a host can warn before it calls a file safe
1172
+ to share:
1173
+
1174
+ ```ts
1175
+ const out = exportSpec(spec);
1176
+ for (const { path, server, sign } of findSpecSecrets(out)) {
1177
+ warn(`${server}: ${path} may hold a credential (${sign})`); // bundle.mcpServers[0].args[2] … (flag)
1178
+ }
1179
+ ```
1180
+
1181
+ A hint is a place and a reason — `SecretSign.Userinfo`, `Query`, `Flag`, `Value` or `Field` — and
1182
+ never the value. It is a guess in both directions: a credential in a positional argument with no
1183
+ telling prefix is missed, and a flag called `--keyboard` is named. A `${VAR}` is a reference, and is
1184
+ not reported.
1185
+
1168
1186
  `bundle.mcpServers` is parsed only under `parseSpec(document, { bundle: true })`. A bundled server
1169
1187
  is a command line and an environment map, so importing one is equivalent to running a program, and
1170
1188
  that is a decision a host makes rather than inherits from a default.
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export { preview } from './run/run-calls.ts';
25
25
  export { resetAll } from './runtime/reset.ts';
26
26
  export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from './runtime/runtime.ts';
27
27
  export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from './runtime/snapshot.ts';
28
- export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec/spec.ts';
28
+ export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, findSpecSecrets, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, type SecretHint, SecretSign, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from './spec/spec.ts';
29
29
  export type { CatalogServer } from './tools/catalog.ts';
30
30
  export { type KeywordPreselection, type KeywordPreselectOptions, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, type ToolMatch, } from './tools/preselect.ts';
31
31
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolOrder, } from './tools/tool-loading.ts';
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export { preview } from './run/run-calls.js';
25
25
  export { resetAll } from './runtime/reset.js';
26
26
  export { createRuntime, defaultRuntime } from './runtime/runtime.js';
27
27
  export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from './runtime/snapshot.js';
28
- export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from './spec/spec.js';
28
+ export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, findSpecSecrets, parseSpec, REF_FILE, REF_URL, RESOLVED_DEFAULTS, resolveAgentSpec, SecretSign, SPEC_EVENTS, } from './spec/spec.js';
29
29
  export { PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselect, preselectByKeywords, preselectInput, preselection, preselectSystem, } from './tools/preselect.js';
30
30
  export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, orderTools, PROXY_TOOLS, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from './tools/tool-loading.js';
31
31
  export { continueTurn, } from './turn/continuation.js';
@@ -359,7 +359,47 @@ export interface ExportSpecOptions {
359
359
  * The property worth keeping is that **a redacted spec is still a valid spec** — it parses, it
360
360
  * resolves, and what comes back is the same agent pointed at the same servers, needing only the
361
361
  * credentials the importing host supplies itself. So a bundled server loses its `env` and its
362
- * `headers` and nothing else, and nothing outside `bundle` is touched: the format has no
363
- * credential field at any depth, which is the reason there is nothing else to strip.
362
+ * `headers` and nothing else, and nothing outside `bundle` is touched: those two are the only
363
+ * fields the format gives a credential. A server can still carry one where the format gives it no
364
+ * field — an `--api-key` among its `args`, a token in its `url` — and that is kept, because
365
+ * cutting it would point the agent at a different server. `findSpecSecrets` says where to look.
364
366
  */
365
367
  export declare function exportSpec(spec: AgentSpec, { secrets }?: ExportSpecOptions): AgentSpec;
368
+ /** Why `findSpecSecrets` named a place. */
369
+ export declare const SecretSign: {
370
+ /** A URL with a user or a password before its host. */
371
+ readonly Userinfo: 'userinfo';
372
+ /** A URL query parameter whose name reads as a credential's. */
373
+ readonly Query: 'query';
374
+ /** The value of a command-line flag, or of a header written as `Name: value`, so named. */
375
+ readonly Flag: 'flag';
376
+ /** A value with the shape of a credential: a known key prefix, or `Bearer` and a token. */
377
+ readonly Value: 'value';
378
+ /** A key of the server itself whose name reads as a credential's. */
379
+ readonly Field: 'field';
380
+ };
381
+ /** Any one of the reasons in `SecretSign`. */
382
+ export type SecretSign = (typeof SecretSign)[keyof typeof SecretSign];
383
+ /** One place `findSpecSecrets` thinks a credential may be, and never the credential. */
384
+ export interface SecretHint {
385
+ /** Where, in `parseSpec`'s spelling: `bundle.mcpServers[0].args[2]`. */
386
+ path: string;
387
+ /** The server's slug, or its id where it has no slug. */
388
+ server: string;
389
+ sign: SecretSign;
390
+ }
391
+ /**
392
+ * Where a document's bundled servers may still carry a credential after `exportSpec`.
393
+ *
394
+ * @param spec - A document, exported or not: `env` and `headers` are not reported either way,
395
+ * since `exportSpec` strips them.
396
+ * @returns One hint for each suspect place, in document order, each naming where and why and
397
+ * none holding the value. Empty for a document with no bundle.
398
+ *
399
+ * @remarks
400
+ * A host calls this before it offers an export as safe to share, and warns with what comes back.
401
+ * It reads a server's `url`, its `command` and `args`, and the names of its other keys, and is a
402
+ * guess both ways: a credential in a positional argument with no telling prefix is missed, and a
403
+ * flag called `--keyboard` is named. A `${VAR}` is a reference and is never named.
404
+ */
405
+ export declare function findSpecSecrets(spec: AgentSpec): SecretHint[];
package/dist/spec/spec.js CHANGED
@@ -896,8 +896,10 @@ export function resolveAgentSpec(layers) {
896
896
  * The property worth keeping is that **a redacted spec is still a valid spec** — it parses, it
897
897
  * resolves, and what comes back is the same agent pointed at the same servers, needing only the
898
898
  * credentials the importing host supplies itself. So a bundled server loses its `env` and its
899
- * `headers` and nothing else, and nothing outside `bundle` is touched: the format has no
900
- * credential field at any depth, which is the reason there is nothing else to strip.
899
+ * `headers` and nothing else, and nothing outside `bundle` is touched: those two are the only
900
+ * fields the format gives a credential. A server can still carry one where the format gives it no
901
+ * field — an `--api-key` among its `args`, a token in its `url` — and that is kept, because
902
+ * cutting it would point the agent at a different server. `findSpecSecrets` says where to look.
901
903
  */
902
904
  export function exportSpec(spec, { secrets = false } = {}) {
903
905
  if (secrets || !spec.bundle?.mcpServers) {
@@ -911,3 +913,160 @@ export function exportSpec(spec, { secrets = false } = {}) {
911
913
  },
912
914
  };
913
915
  }
916
+ /** Why `findSpecSecrets` named a place. */
917
+ export const SecretSign = {
918
+ /** A URL with a user or a password before its host. */
919
+ Userinfo: 'userinfo',
920
+ /** A URL query parameter whose name reads as a credential's. */
921
+ Query: 'query',
922
+ /** The value of a command-line flag, or of a header written as `Name: value`, so named. */
923
+ Flag: 'flag',
924
+ /** A value with the shape of a credential: a known key prefix, or `Bearer` and a token. */
925
+ Value: 'value',
926
+ /** A key of the server itself whose name reads as a credential's. */
927
+ Field: 'field',
928
+ };
929
+ /** A name that reads as a credential's: `api-key`, `TOKEN`, `Authorization`. */
930
+ const SECRET_NAME = /key|token|secret|passw|auth|credential|bearer/i;
931
+ /** A value shaped like a credential, whatever it is called. */
932
+ const SECRET_VALUE = /^(sk-|pk-|rk-|ghp_|gho_|ghs_|github_pat_|glpat-|xox[abprs]-|AKIA)|\b(bearer|basic)\s+\S/i;
933
+ /** What an absolute URL opens with. */
934
+ const URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
935
+ /** A header as one string, `Name: value`, as `--header` takes it. */
936
+ const HEADER = /^([\w-]+):\s*(\S.*)$/;
937
+ /** A flag carrying its own value, `--name=value`. */
938
+ const FLAG_WITH_VALUE = /^(-{1,2}[^=\s]+)=(.+)$/;
939
+ /** The keys of a server `findSpecSecrets` reads for itself, or `exportSpec` strips. */
940
+ const SERVER_KEYS_READ = new Set(['id', 'slug', 'url', 'command', 'args', 'env', 'headers']);
941
+ /**
942
+ * Whether a string is a credential written out, as against a reference to one.
943
+ *
944
+ * @param value - What a field, a flag or a parameter holds.
945
+ * @returns `false` for an empty string and for one holding a `${VAR}`, which names where the
946
+ * credential is and is not it.
947
+ */
948
+ const writtenOut = (value) => value !== '' && VARIABLE.test(value) === false;
949
+ /**
950
+ * Why a URL may carry a credential.
951
+ *
952
+ * @param text - A string, which need not be a URL.
953
+ * @returns `Userinfo`, `Query`, or `undefined` for a URL that shows neither and for a string
954
+ * that is not an absolute URL.
955
+ */
956
+ function urlSign(text) {
957
+ if (URL_SCHEME.test(text) === false || URL.canParse(text) === false) {
958
+ return undefined;
959
+ }
960
+ const url = new URL(text);
961
+ if (writtenOut(url.username) || writtenOut(url.password)) {
962
+ return SecretSign.Userinfo;
963
+ }
964
+ for (const [name, value] of url.searchParams) {
965
+ if (SECRET_NAME.test(name) && writtenOut(value)) {
966
+ return SecretSign.Query;
967
+ }
968
+ }
969
+ return undefined;
970
+ }
971
+ /**
972
+ * Why one word of a command line may be a credential, read on its own.
973
+ *
974
+ * @param word - One argument.
975
+ * @returns A sign, or `undefined` for a word that shows none. A flag with no value of its own
976
+ * shows none here: `argumentSigns` reads the word after it.
977
+ */
978
+ function wordSign(word) {
979
+ if (writtenOut(word) === false) {
980
+ return undefined;
981
+ }
982
+ const flag = FLAG_WITH_VALUE.exec(word);
983
+ if (flag?.[1] !== undefined && SECRET_NAME.test(flag[1])) {
984
+ return SecretSign.Flag;
985
+ }
986
+ const header = HEADER.exec(word);
987
+ if (header?.[1] !== undefined && SECRET_NAME.test(header[1])) {
988
+ return SecretSign.Flag;
989
+ }
990
+ // A flag's own value is read as a word too: `--url=https://user:pass@host`.
991
+ const value = flag?.[2] ?? word;
992
+ return SECRET_VALUE.test(value) ? SecretSign.Value : urlSign(value);
993
+ }
994
+ /**
995
+ * The words of a command line that may be credentials.
996
+ *
997
+ * @param words - The arguments in order. One that is not a string is passed over.
998
+ * @returns Each suspect word's index and its sign, in order, at most one sign a word.
999
+ *
1000
+ * @remarks
1001
+ * A flag named like a credential marks the word after it, unless that word is another flag.
1002
+ */
1003
+ function argumentSigns(words) {
1004
+ const signs = [];
1005
+ let flagged = false;
1006
+ for (const [index, word] of words.entries()) {
1007
+ if (typeof word !== 'string') {
1008
+ flagged = false;
1009
+ continue;
1010
+ }
1011
+ const isFlag = word.startsWith('-');
1012
+ const sign = flagged && isFlag === false && writtenOut(word) ? SecretSign.Flag : wordSign(word);
1013
+ if (sign) {
1014
+ signs.push([index, sign]);
1015
+ }
1016
+ flagged = isFlag && word.includes('=') === false && SECRET_NAME.test(word);
1017
+ }
1018
+ return signs;
1019
+ }
1020
+ /**
1021
+ * The places one bundled server may carry a credential, outside its `env` and `headers`.
1022
+ *
1023
+ * @param server - The server as the document holds it.
1024
+ * @param path - Where the server is, which each hint's path opens with.
1025
+ * @returns The hints in the order `url`, `command`, `args`, then the server's other keys.
1026
+ */
1027
+ function serverSecrets(server, path) {
1028
+ const name = server.slug ?? server.id ?? '';
1029
+ const hints = [];
1030
+ const hint = (at, sign) => {
1031
+ if (sign) {
1032
+ hints.push({ path: `${path}.${at}`, server: name, sign });
1033
+ }
1034
+ };
1035
+ if (typeof server.url === 'string') {
1036
+ hint('url', urlSign(server.url));
1037
+ }
1038
+ if (typeof server.command === 'string') {
1039
+ // A command written as one line is read as its words, and named once.
1040
+ hint('command', argumentSigns(server.command.split(/\s+/))[0]?.[1]);
1041
+ }
1042
+ if (Array.isArray(server.args)) {
1043
+ for (const [index, sign] of argumentSigns(server.args)) {
1044
+ hint(`args[${index}]`, sign);
1045
+ }
1046
+ }
1047
+ for (const [key, value] of Object.entries(server)) {
1048
+ const empty = absent(value) || value === '' || (typeof value === 'string' && writtenOut(value) === false);
1049
+ if (SERVER_KEYS_READ.has(key) === false && SECRET_NAME.test(key) && empty === false) {
1050
+ hint(key, SecretSign.Field);
1051
+ }
1052
+ }
1053
+ return hints;
1054
+ }
1055
+ /**
1056
+ * Where a document's bundled servers may still carry a credential after `exportSpec`.
1057
+ *
1058
+ * @param spec - A document, exported or not: `env` and `headers` are not reported either way,
1059
+ * since `exportSpec` strips them.
1060
+ * @returns One hint for each suspect place, in document order, each naming where and why and
1061
+ * none holding the value. Empty for a document with no bundle.
1062
+ *
1063
+ * @remarks
1064
+ * A host calls this before it offers an export as safe to share, and warns with what comes back.
1065
+ * It reads a server's `url`, its `command` and `args`, and the names of its other keys, and is a
1066
+ * guess both ways: a credential in a positional argument with no telling prefix is missed, and a
1067
+ * flag called `--keyboard` is named. A `${VAR}` is a reference and is never named.
1068
+ */
1069
+ export function findSpecSecrets(spec) {
1070
+ const servers = spec.bundle?.mcpServers ?? [];
1071
+ return servers.flatMap((server, index) => serverSecrets(server, `bundle.mcpServers[${index}]`));
1072
+ }
package/llms.txt CHANGED
@@ -220,6 +220,7 @@ A JSON document that defines an agent, and the rules for reading one.
220
220
  - `EndpointSpec` (type) — Where a model is reached, and how long to wait for it.
221
221
  - `ExportSpecOptions` (type) — What `exportSpec` takes besides the document.
222
222
  - `exportSpec` — A document fit to leave the host: the same agent, with the secrets of its bundled servers gone.
223
+ - `findSpecSecrets` — Where a document's bundled servers may still carry a credential after `exportSpec`.
223
224
  - `ModelSpec` (type) — Which model answers and how, including the request fields this interface cannot spell.
224
225
  - `ParsedSpec` (type) — What `parseSpec` hands back.
225
226
  - `ParseSpecOptions` (type) — What `parseSpec` takes besides the document.
@@ -232,6 +233,8 @@ A JSON document that defines an agent, and the rules for reading one.
232
233
  - `ResolvedTask` (type) — One side task, resolved: a model, and the endpoint it is reached through.
233
234
  - `RetrySpec` (type) — What to do about a request lost before the model produced anything.
234
235
  - `resolveAgentSpec` — Layers documents into the flat object the loop takes, weakest first.
236
+ - `SecretHint` (type) — One place `findSpecSecrets` thinks a credential may be, and never the credential.
237
+ - `SecretSign` — Any one of the reasons in `SecretSign`.
235
238
  - `SPEC_EVENTS` — Every event a hook may be bound to: `HOOK_EVENTS`, under the name a document's reader knows.
236
239
  - `SpecBundle` (type) — What an agent carries so it can travel whole: the definitions of the servers it names.
237
240
  - `SpecServer` (type) — A bundled MCP server, validated only as far as a round trip needs and otherwise passed through.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",