@safekb/mcp 0.10.1 → 0.12.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
@@ -371,6 +371,19 @@ custom fields and the key name of every custom field (sealed ones too) stay read
371
371
  space takes its credentials along (a credential's group in the old space is left behind).
372
372
  - `reveal_sealed_credential` and `get_sealed_totp` decrypt locally. The hosted `reveal_credential` and
373
373
  `get_credential_totp` cannot open a sealed credential.
374
+ - AI access: `visible`, `use_only` or `human_only`, per credential (`llm_visibility`) and per sealed
375
+ field (`llm_field_visibility`, e.g. a card's number and CVC hidden, its expiry date visible).
376
+ `reveal_sealed_credential` and `get_sealed_totp` work only on a `visible` field. New and migrated
377
+ sealed credentials start with the tenant default (Settings → Sealed credentials; `use_only` until
378
+ set). Only you make anything more visible, in the SafeKB app; `set_sealed_credential_llm_visibility`
379
+ only tightens. The client enforces this: a client older than 0.11.0 reveals every sealed credential, and 0.11.0 ignores field overrides.
380
+ - Using a secret without showing it (0.12.0): `secret_exec` runs a program with it in env or stdin
381
+ (never argv), `secret_http` sends one request with it in the url, headers or body (redirects are not
382
+ followed), and `secret_adb_input` types it on an Android device (`adb shell input text`, value on stdin;
383
+ printable ASCII only). The agent names it by handle, `{{secret://<credential_id>/<field>}}`; a `totp`
384
+ handle becomes the current code. `visible` and `use_only` fields can be used; `human_only` fields never.
385
+ Output comes back scrubbed of the value (as is, URL-encoded, base64, hex, JSON-escaped) with the
386
+ credentials and fields used. Scrubbing stops careless leaks, not a program written to leak one.
374
387
  - Visible fields can be edited without the secret. Such an edit is flagged (`visible_tampered`) with the
375
388
  changed fields until a signed save accepts it (`accept_visible_changes`).
376
389
 
@@ -2581,7 +2581,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2581
2581
  // package.json
2582
2582
  var package_default = {
2583
2583
  name: "@safekb/mcp",
2584
- version: "0.10.1",
2584
+ version: "0.12.0",
2585
2585
  description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
2586
2586
  license: "SEE LICENSE IN LICENSE",
2587
2587
  author: "Mustafa Öztürk",
@@ -3276,6 +3276,12 @@ class SealedApi {
3276
3276
  fetchSealedCredential(id) {
3277
3277
  return this.http.getJson(`/credentials/${id}/sealed`);
3278
3278
  }
3279
+ setLlmVisibility(id, llmVisibility, field) {
3280
+ return this.http.putJson(`/credentials/${id}/sealed/llm-visibility`, {
3281
+ llm_visibility: llmVisibility,
3282
+ ...field === undefined ? {} : { field }
3283
+ });
3284
+ }
3279
3285
  lastAttestedVisibleFields(id) {
3280
3286
  return this.http.getJson(`/credentials/${id}/sealed/attested`);
3281
3287
  }
@@ -8909,6 +8915,259 @@ class LocalKeys {
8909
8915
  }
8910
8916
  }
8911
8917
 
8918
+ // src/sealed/credentialAccess.ts
8919
+ var REVEALABLE = new Set(["visible"]);
8920
+ var USABLE = new Set(["visible", "use_only"]);
8921
+ var CUSTOM_PREFIX2 = customFieldName("");
8922
+ function visibleOf(credential2) {
8923
+ return {
8924
+ title: credential2.title,
8925
+ username: credential2.username,
8926
+ url: credential2.url,
8927
+ tags: credential2.tags,
8928
+ notes: credential2.notes,
8929
+ custom: credential2.custom.flatMap(({ key, value }) => value === undefined ? [] : [{ key, value }])
8930
+ };
8931
+ }
8932
+ function sealedValue(open5, field) {
8933
+ const { values, credential: credential2 } = open5;
8934
+ const value = field.startsWith(CUSTOM_PREFIX2) ? values.custom.find((item) => item.key === field.slice(CUSTOM_PREFIX2.length))?.value : SEALED_CREDENTIAL_FIELDS.includes(field) ? values[field] : undefined;
8935
+ if (value === undefined || value === null) {
8936
+ throw new ClientError(`Credential ${credential2.id} has no sealed field '${field}'. Sealed fields: password, totp, sealed_notes, custom:{key}.`);
8937
+ }
8938
+ return value;
8939
+ }
8940
+ function assertDistinctCustomKeys(visible, values) {
8941
+ const keys = [...visible.custom, ...values.custom].map(({ key }) => key);
8942
+ if (keys.some((key) => key.length === 0)) {
8943
+ throw new ClientError("A custom field needs a key");
8944
+ }
8945
+ if (new Set(keys).size !== keys.length) {
8946
+ throw new ClientError("Custom field keys must be unique");
8947
+ }
8948
+ }
8949
+
8950
+ class CredentialAccess {
8951
+ ctx;
8952
+ constructor(ctx) {
8953
+ this.ctx = ctx;
8954
+ }
8955
+ context(credentialId) {
8956
+ return { tenant: this.ctx.tenant, credential: credentialId };
8957
+ }
8958
+ async newDek(rootId, credentialId) {
8959
+ const { keys } = await this.ctx.session.open(rootId);
8960
+ const dek = generateDek();
8961
+ return {
8962
+ dek,
8963
+ encDek: serializeEnvelope(await wrapDek(keys, credentialId, dek))
8964
+ };
8965
+ }
8966
+ async rewrapDek(dek, rootId, credentialId) {
8967
+ const { keys } = await this.ctx.session.open(rootId);
8968
+ return serializeEnvelope(await wrapDek(keys, credentialId, dek));
8969
+ }
8970
+ async open(credentialId) {
8971
+ return this.decrypt(await this.ctx.api.fetchSealedCredential(credentialId));
8972
+ }
8973
+ async openForReveal(credentialId, field) {
8974
+ return this.openAllowing(credentialId, field, REVEALABLE, (visibility) => visibility === "use_only" ? "it is never shown to an LLM. Use it without seeing it: secret_exec, secret_http or secret_adb_input with the handle secret://<credential_id>/<field>." : "it is never shown to an LLM and never handed to a tool: ask the user to enter it.");
8975
+ }
8976
+ async openForUse(credentialId, field) {
8977
+ return this.openAllowing(credentialId, field, USABLE, () => "no tool may use it: ask the user to enter it.");
8978
+ }
8979
+ async openAllowing(credentialId, field, allowed, refusal) {
8980
+ const credential2 = await this.ctx.api.fetchSealedCredential(credentialId);
8981
+ const visibility = Object.hasOwn(credential2.llm_field_visibility, field) ? credential2.llm_field_visibility[field] : credential2.llm_visibility;
8982
+ if (!allowed.has(visibility)) {
8983
+ throw new ClientError(`Field '${field}' of credential ${credentialId} (${credential2.title}) is ${visibility}: ${refusal(visibility)} Only the user can change this, in the SafeKB app.`);
8984
+ }
8985
+ return this.decrypt(credential2);
8986
+ }
8987
+ async decrypt(credential2) {
8988
+ const credentialId = credential2.id;
8989
+ const { keys } = await this.ctx.session.open(credential2.seal_root_id);
8990
+ const dek = await unwrapDek(keys, credentialId, parseEnvelope(credential2.enc_dek));
8991
+ const values = await unsealCredentialFields(dek, this.context(credentialId), credential2.enc_fields);
8992
+ return { credential: credential2, dek, values };
8993
+ }
8994
+ async seal(credentialId, dek, visible, values) {
8995
+ assertDistinctCustomKeys(visible, values);
8996
+ return {
8997
+ visible,
8998
+ encFields: await sealCredentialFields(dek, this.context(credentialId), values),
8999
+ visibleDigest: await visibleFieldsDigest(visible)
9000
+ };
9001
+ }
9002
+ async sign(message, signerRootIds) {
9003
+ const signers = [];
9004
+ for (const rootId of signerRootIds) {
9005
+ signers.push((await this.ctx.session.open(rootId)).keys);
9006
+ }
9007
+ return signMessage(await buildCredentialMessage(message), signers);
9008
+ }
9009
+ }
9010
+
9011
+ // src/sinks/redaction.ts
9012
+ var MAX_SHOWN_CHARS2 = 32 * 1024;
9013
+ function encodedForms(value) {
9014
+ const bytes = Buffer.from(value, "utf8");
9015
+ const base64 = bytes.toString("base64");
9016
+ const hex = bytes.toString("hex");
9017
+ return [
9018
+ value,
9019
+ encodeURIComponent(value),
9020
+ new URLSearchParams({ v: value }).toString().slice(2),
9021
+ value.replaceAll(" ", "%s"),
9022
+ value.replaceAll(" ", "%s").replaceAll("'", `'\\''`),
9023
+ JSON.stringify(value).slice(1, -1),
9024
+ base64,
9025
+ base64.replace(/=+$/, ""),
9026
+ bytes.toString("base64url"),
9027
+ hex,
9028
+ hex.toUpperCase()
9029
+ ];
9030
+ }
9031
+ var escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9032
+
9033
+ class Redactor {
9034
+ markers;
9035
+ pattern;
9036
+ longest;
9037
+ constructor(secrets) {
9038
+ this.markers = new Map;
9039
+ for (const { name, value } of secrets) {
9040
+ for (const form of encodedForms(value)) {
9041
+ if (form.length > 0)
9042
+ this.markers.set(form, `«redacted:${name}»`);
9043
+ }
9044
+ }
9045
+ const forms = [...this.markers.keys()].sort((a, b) => b.length - a.length);
9046
+ this.pattern = forms.length === 0 ? null : new RegExp(forms.map(escapeRegex).join("|"), "g");
9047
+ this.longest = forms[0]?.length ?? 0;
9048
+ }
9049
+ redact(text) {
9050
+ if (this.pattern === null)
9051
+ return text;
9052
+ return text.replace(this.pattern, (form) => this.markers.get(form));
9053
+ }
9054
+ shown(text, cut) {
9055
+ const whole = cut ? text.slice(0, Math.max(0, text.length - this.longest)) : text;
9056
+ const scrubbed = this.redact(whole);
9057
+ return {
9058
+ text: scrubbed.slice(0, MAX_SHOWN_CHARS2),
9059
+ truncated: cut || scrubbed.length > MAX_SHOWN_CHARS2
9060
+ };
9061
+ }
9062
+ }
9063
+
9064
+ // src/sinks/secretHandles.ts
9065
+ var HANDLE = /\{\{secret:\/\/([0-9a-f]{24})\/([^{}]+)\}\}/g;
9066
+ var ANY_HANDLE = /secret:\/\//;
9067
+ var handleKey = ({ credentialId, field }) => `${credentialId}/${field}`;
9068
+ function refsIn(text) {
9069
+ const refs = [];
9070
+ const rest = text.replace(HANDLE, (_, credentialId, field) => {
9071
+ refs.push({ credentialId, field });
9072
+ return "";
9073
+ });
9074
+ if (ANY_HANDLE.test(rest)) {
9075
+ throw new ClientError("A secret handle must be written {{secret://<24-character credential id>/<field>}}, e.g. {{secret://0123456789abcdef01234567/password}}");
9076
+ }
9077
+ return refs;
9078
+ }
9079
+ function hasHandle(text) {
9080
+ return ANY_HANDLE.test(text);
9081
+ }
9082
+ function fillHandles(text, lookup) {
9083
+ return text.replace(HANDLE, (_, credentialId, field) => lookup({ credentialId, field }));
9084
+ }
9085
+ function requireHandlesAfterOrigin(url) {
9086
+ const first = url.search(ANY_HANDLE);
9087
+ if (first === -1)
9088
+ return;
9089
+ if (!/^https?:\/\/[^/?#{}]+[/?#]/.test(url.slice(0, first))) {
9090
+ throw new ClientError("A secret handle can go in the path, query or fragment of a url, never in its scheme, host or port.");
9091
+ }
9092
+ }
9093
+
9094
+ // src/sinks/secretResolver.ts
9095
+ var TOTP_FIELD = "totp";
9096
+ var MIN_TOTP_SECONDS = 5;
9097
+ var SYSTEM_CLOCK = {
9098
+ now: () => Date.now(),
9099
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
9100
+ };
9101
+
9102
+ class ResolvedSecrets {
9103
+ byKey;
9104
+ redactor;
9105
+ constructor(resolved) {
9106
+ this.byKey = new Map(resolved.map((item) => [handleKey(item.ref), item]));
9107
+ this.redactor = new Redactor(resolved.map(({ ref, value }) => ({ name: handleKey(ref), value })));
9108
+ }
9109
+ fill(text, encode = (v) => v) {
9110
+ return fillHandles(text, (ref) => encode(this.valueOf(ref)));
9111
+ }
9112
+ valueOf(ref) {
9113
+ const resolved = this.byKey.get(handleKey(ref));
9114
+ if (resolved === undefined) {
9115
+ throw new Error(`Handle ${handleKey(ref)} was not resolved`);
9116
+ }
9117
+ return resolved.value;
9118
+ }
9119
+ redact(text) {
9120
+ return this.redactor.redact(text);
9121
+ }
9122
+ shown(text, cut) {
9123
+ return this.redactor.shown(text, cut);
9124
+ }
9125
+ used() {
9126
+ return [...this.byKey.values()].map(({ ref, title }) => ({
9127
+ credential_id: ref.credentialId,
9128
+ title,
9129
+ field: ref.field
9130
+ }));
9131
+ }
9132
+ }
9133
+
9134
+ class SecretResolver {
9135
+ access;
9136
+ clock;
9137
+ constructor(access, clock = SYSTEM_CLOCK) {
9138
+ this.access = access;
9139
+ this.clock = clock;
9140
+ }
9141
+ async resolve(texts) {
9142
+ const refs = new Map;
9143
+ for (const ref of texts.flatMap(refsIn))
9144
+ refs.set(handleKey(ref), ref);
9145
+ const ordered = [...refs.values()].sort((a, b) => Number(a.field === TOTP_FIELD) - Number(b.field === TOTP_FIELD));
9146
+ const resolved = [];
9147
+ for (const ref of ordered)
9148
+ resolved.push(await this.resolveOne(ref));
9149
+ return new ResolvedSecrets(resolved);
9150
+ }
9151
+ async resolveOne(ref) {
9152
+ const open5 = await this.access.openForUse(ref.credentialId, ref.field);
9153
+ const value = sealedValue(open5, ref.field);
9154
+ return {
9155
+ ref,
9156
+ title: open5.credential.title,
9157
+ value: ref.field === TOTP_FIELD ? await this.freshCode(value) : value
9158
+ };
9159
+ }
9160
+ async freshCode(seed) {
9161
+ const params = parseTotp(seed);
9162
+ for (;; ) {
9163
+ const current = await totp(params, this.clock.now());
9164
+ if (current.secondsRemaining > MIN_TOTP_SECONDS)
9165
+ return current.code;
9166
+ await this.clock.sleep(current.secondsRemaining * 1000);
9167
+ }
9168
+ }
9169
+ }
9170
+
8912
9171
  // src/sealed/content.ts
8913
9172
  function metaOf(content) {
8914
9173
  return {
@@ -9247,71 +9506,6 @@ class CreateService {
9247
9506
  }
9248
9507
  }
9249
9508
 
9250
- // src/sealed/credentialAccess.ts
9251
- function visibleOf(credential2) {
9252
- return {
9253
- title: credential2.title,
9254
- username: credential2.username,
9255
- url: credential2.url,
9256
- tags: credential2.tags,
9257
- notes: credential2.notes,
9258
- custom: credential2.custom.flatMap(({ key, value }) => value === undefined ? [] : [{ key, value }])
9259
- };
9260
- }
9261
- function assertDistinctCustomKeys(visible, values) {
9262
- const keys = [...visible.custom, ...values.custom].map(({ key }) => key);
9263
- if (keys.some((key) => key.length === 0)) {
9264
- throw new ClientError("A custom field needs a key");
9265
- }
9266
- if (new Set(keys).size !== keys.length) {
9267
- throw new ClientError("Custom field keys must be unique");
9268
- }
9269
- }
9270
-
9271
- class CredentialAccess {
9272
- ctx;
9273
- constructor(ctx) {
9274
- this.ctx = ctx;
9275
- }
9276
- context(credentialId) {
9277
- return { tenant: this.ctx.tenant, credential: credentialId };
9278
- }
9279
- async newDek(rootId, credentialId) {
9280
- const { keys } = await this.ctx.session.open(rootId);
9281
- const dek = generateDek();
9282
- return {
9283
- dek,
9284
- encDek: serializeEnvelope(await wrapDek(keys, credentialId, dek))
9285
- };
9286
- }
9287
- async rewrapDek(dek, rootId, credentialId) {
9288
- const { keys } = await this.ctx.session.open(rootId);
9289
- return serializeEnvelope(await wrapDek(keys, credentialId, dek));
9290
- }
9291
- async open(credentialId) {
9292
- const credential2 = await this.ctx.api.fetchSealedCredential(credentialId);
9293
- const { keys } = await this.ctx.session.open(credential2.seal_root_id);
9294
- const dek = await unwrapDek(keys, credentialId, parseEnvelope(credential2.enc_dek));
9295
- const values = await unsealCredentialFields(dek, this.context(credentialId), credential2.enc_fields);
9296
- return { credential: credential2, dek, values };
9297
- }
9298
- async seal(credentialId, dek, visible, values) {
9299
- assertDistinctCustomKeys(visible, values);
9300
- return {
9301
- visible,
9302
- encFields: await sealCredentialFields(dek, this.context(credentialId), values),
9303
- visibleDigest: await visibleFieldsDigest(visible)
9304
- };
9305
- }
9306
- async sign(message, signerRootIds) {
9307
- const signers = [];
9308
- for (const rootId of signerRootIds) {
9309
- signers.push((await this.ctx.session.open(rootId)).keys);
9310
- }
9311
- return signMessage(await buildCredentialMessage(message), signers);
9312
- }
9313
- }
9314
-
9315
9509
  // src/sealed/secretScan.ts
9316
9510
  var PEM_BLOCK = /-----BEGIN [A-Z0-9 ]+-----/;
9317
9511
  var SECRET_WORD = "pass(?:word|wd|phrase|code|wort)?|kennwort|geheim(?:nis)?|pwd|pin|puk|tan|secret|token|api[ _-]?key|access[ _-]?key|private[ _-]?key|client[ _-]?secret|bearer|recovery[ _-]?codes?|backup[ _-]?codes?|seed|cvv|cvc";
@@ -9688,7 +9882,6 @@ class CredentialMigration {
9688
9882
 
9689
9883
  // src/sealed/credentialService.ts
9690
9884
  var NOT_FOUND2 = 404;
9691
- var CUSTOM_PREFIX2 = customFieldName("");
9692
9885
  var VISIBLE_NAMES = [
9693
9886
  "title",
9694
9887
  "username",
@@ -9736,8 +9929,8 @@ class CredentialService {
9736
9929
  this.access = access;
9737
9930
  }
9738
9931
  async reveal(credentialId, field) {
9739
- const open5 = await this.access.open(credentialId);
9740
- const value = this.valueOf(open5, field);
9932
+ const open5 = await this.access.openForReveal(credentialId, field);
9933
+ const value = sealedValue(open5, field);
9741
9934
  return {
9742
9935
  id: credentialId,
9743
9936
  version: open5.credential.version,
@@ -9747,8 +9940,8 @@ class CredentialService {
9747
9940
  };
9748
9941
  }
9749
9942
  async totp(credentialId, nowMs = Date.now()) {
9750
- const open5 = await this.access.open(credentialId);
9751
- const code = await totp(parseTotp(this.valueOf(open5, "totp")), nowMs);
9943
+ const open5 = await this.access.openForReveal(credentialId, "totp");
9944
+ const code = await totp(parseTotp(sealedValue(open5, "totp")), nowMs);
9752
9945
  return {
9753
9946
  id: credentialId,
9754
9947
  code: code.code,
@@ -9756,13 +9949,13 @@ class CredentialService {
9756
9949
  ...await this.attestation(open5.credential)
9757
9950
  };
9758
9951
  }
9759
- valueOf(open5, field) {
9760
- const { values, credential: credential2 } = open5;
9761
- const value = field.startsWith(CUSTOM_PREFIX2) ? values.custom.find((item) => item.key === field.slice(CUSTOM_PREFIX2.length))?.value : SEALED_CREDENTIAL_FIELDS.includes(field) ? values[field] : undefined;
9762
- if (value === undefined || value === null) {
9763
- throw new ClientError(`Credential ${credential2.id} has no sealed field '${field}'. Sealed fields: password, totp, sealed_notes, custom:{key}.`);
9764
- }
9765
- return value;
9952
+ async setLlmVisibility(credentialId, llmVisibility, field) {
9953
+ const credential2 = await this.ctx.api.setLlmVisibility(credentialId, llmVisibility, field);
9954
+ return {
9955
+ id: credential2.id,
9956
+ llm_visibility: credential2.llm_visibility,
9957
+ llm_field_visibility: credential2.llm_field_visibility
9958
+ };
9766
9959
  }
9767
9960
  async attestation(credential2) {
9768
9961
  const current = visibleOf(credential2);
@@ -10720,7 +10913,8 @@ function createSealedServices(ctx) {
10720
10913
  attachments: new AttachmentService(ctx, access),
10721
10914
  deposits: new DepositService(ctx, access, tree, documents),
10722
10915
  credentials: new CredentialService(ctx, credentialAccess),
10723
- migration: new CredentialMigration(ctx, credentialAccess)
10916
+ migration: new CredentialMigration(ctx, credentialAccess),
10917
+ secrets: new SecretResolver(credentialAccess)
10724
10918
  };
10725
10919
  }
10726
10920
 
@@ -12750,7 +12944,7 @@ __export(exports_util, {
12750
12944
  floatSafeRemainder: () => floatSafeRemainder,
12751
12945
  finalizeIssue: () => finalizeIssue,
12752
12946
  extend: () => extend,
12753
- escapeRegex: () => escapeRegex,
12947
+ escapeRegex: () => escapeRegex2,
12754
12948
  esc: () => esc,
12755
12949
  defineLazy: () => defineLazy,
12756
12950
  createTransparentProxy: () => createTransparentProxy,
@@ -13002,7 +13196,7 @@ var getParsedType = (data) => {
13002
13196
  };
13003
13197
  var propertyKeyTypes = new Set(["string", "number", "symbol"]);
13004
13198
  var primitiveTypes = new Set(["string", "number", "bigint", "boolean", "symbol", "undefined"]);
13005
- function escapeRegex(str) {
13199
+ function escapeRegex2(str) {
13006
13200
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13007
13201
  }
13008
13202
  function clone(inst, def, params) {
@@ -13698,7 +13892,7 @@ function emoji() {
13698
13892
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
13699
13893
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
13700
13894
  var mac = (delimiter) => {
13701
- const escapedDelim = escapeRegex(delimiter ?? ":");
13895
+ const escapedDelim = escapeRegex2(delimiter ?? ":");
13702
13896
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
13703
13897
  };
13704
13898
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -14198,7 +14392,7 @@ var $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (ins
14198
14392
  });
14199
14393
  var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
14200
14394
  $ZodCheck.init(inst, def);
14201
- const escapedRegex = escapeRegex(def.includes);
14395
+ const escapedRegex = escapeRegex2(def.includes);
14202
14396
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
14203
14397
  def.pattern = pattern;
14204
14398
  inst._zod.onattach.push((inst2) => {
@@ -14222,7 +14416,7 @@ var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst,
14222
14416
  });
14223
14417
  var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
14224
14418
  $ZodCheck.init(inst, def);
14225
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
14419
+ const pattern = new RegExp(`^${escapeRegex2(def.prefix)}.*`);
14226
14420
  def.pattern ?? (def.pattern = pattern);
14227
14421
  inst._zod.onattach.push((inst2) => {
14228
14422
  const bag = inst2._zod.bag;
@@ -14245,7 +14439,7 @@ var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (i
14245
14439
  });
14246
14440
  var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
14247
14441
  $ZodCheck.init(inst, def);
14248
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
14442
+ const pattern = new RegExp(`.*${escapeRegex2(def.suffix)}$`);
14249
14443
  def.pattern ?? (def.pattern = pattern);
14250
14444
  inst._zod.onattach.push((inst2) => {
14251
14445
  const bag = inst2._zod.bag;
@@ -15784,7 +15978,7 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
15784
15978
  const values = getEnumValues(def.entries);
15785
15979
  const valuesSet = new Set(values);
15786
15980
  inst._zod.values = valuesSet;
15787
- inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
15981
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex2(o) : o.toString()).join("|")})$`);
15788
15982
  inst._zod.parse = (payload, _ctx) => {
15789
15983
  const input = payload.value;
15790
15984
  if (valuesSet.has(input)) {
@@ -15806,7 +16000,7 @@ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
15806
16000
  }
15807
16001
  const values = new Set(def.values);
15808
16002
  inst._zod.values = values;
15809
- inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
16003
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex2(o) : o ? escapeRegex2(o.toString()) : String(o)).join("|")})$`);
15810
16004
  inst._zod.parse = (payload, _ctx) => {
15811
16005
  const input = payload.value;
15812
16006
  if (values.has(input)) {
@@ -16162,7 +16356,7 @@ var $ZodTemplateLiteral = /* @__PURE__ */ $constructor("$ZodTemplateLiteral", (i
16162
16356
  const end = source.endsWith("$") ? source.length - 1 : source.length;
16163
16357
  regexParts.push(source.slice(start, end));
16164
16358
  } else if (part === null || primitiveTypes.has(typeof part)) {
16165
- regexParts.push(escapeRegex(`${part}`));
16359
+ regexParts.push(escapeRegex2(`${part}`));
16166
16360
  } else {
16167
16361
  throw new Error(`Invalid template literal part: ${part}`);
16168
16362
  }
@@ -35704,6 +35898,271 @@ function keyTools(parts) {
35704
35898
  ];
35705
35899
  }
35706
35900
 
35901
+ // src/sinks/processRunner.ts
35902
+ import { spawn as spawn3 } from "node:child_process";
35903
+ var MAX_CAPTURE_BYTES = 1024 * 1024;
35904
+ var KILL_GRACE_MS = 2000;
35905
+
35906
+ class Capture {
35907
+ chunks = [];
35908
+ size = 0;
35909
+ cut = false;
35910
+ add(chunk) {
35911
+ const room = MAX_CAPTURE_BYTES - this.size;
35912
+ if (chunk.length > room)
35913
+ this.cut = true;
35914
+ if (room <= 0)
35915
+ return;
35916
+ const kept = chunk.subarray(0, room);
35917
+ this.chunks.push(kept);
35918
+ this.size += kept.length;
35919
+ }
35920
+ text() {
35921
+ return Buffer.concat(this.chunks).toString("utf8");
35922
+ }
35923
+ }
35924
+ async function runProcess(request) {
35925
+ const [command, ...args] = request.argv;
35926
+ if (command === undefined) {
35927
+ throw new ClientError("argv needs at least the program to run");
35928
+ }
35929
+ return new Promise((resolve, reject) => {
35930
+ const child = spawn3(command, args, {
35931
+ cwd: request.cwd,
35932
+ env: { ...process.env, ...request.env },
35933
+ stdio: ["pipe", "pipe", "pipe"],
35934
+ detached: true
35935
+ });
35936
+ const killGroup = (signal) => {
35937
+ if (child.pid === undefined)
35938
+ return;
35939
+ try {
35940
+ process.kill(-child.pid, signal);
35941
+ } catch (error48) {
35942
+ if (error48.code !== "ESRCH")
35943
+ throw error48;
35944
+ }
35945
+ };
35946
+ const stdout = new Capture;
35947
+ const stderr = new Capture;
35948
+ let timedOut = false;
35949
+ let stdinError = null;
35950
+ const timer = setTimeout(() => {
35951
+ timedOut = true;
35952
+ killGroup("SIGTERM");
35953
+ setTimeout(() => killGroup("SIGKILL"), KILL_GRACE_MS).unref();
35954
+ }, request.timeoutMs);
35955
+ child.stdout.on("data", (chunk) => stdout.add(chunk));
35956
+ child.stderr.on("data", (chunk) => stderr.add(chunk));
35957
+ child.stdin.on("error", (error48) => {
35958
+ if (error48.code !== "EPIPE")
35959
+ stdinError = error48;
35960
+ });
35961
+ child.once("error", (error48) => {
35962
+ clearTimeout(timer);
35963
+ reject(new ClientError(error48.code === "ENOENT" ? `${command} was not found${request.cwd ? ` (cwd ${request.cwd})` : ""}` : `${command} could not be started: ${error48.code ?? "unknown error"}`));
35964
+ });
35965
+ child.once("close", (exitCode, signal) => {
35966
+ clearTimeout(timer);
35967
+ if (stdinError !== null) {
35968
+ reject(new ClientError(`stdin of ${command} could not be written: ${stdinError.code}`));
35969
+ return;
35970
+ }
35971
+ resolve({
35972
+ exitCode,
35973
+ signal,
35974
+ timedOut,
35975
+ stdout: stdout.text(),
35976
+ stderr: stderr.text(),
35977
+ stdoutCut: stdout.cut,
35978
+ stderrCut: stderr.cut
35979
+ });
35980
+ });
35981
+ child.stdin.end(request.stdin ?? "");
35982
+ });
35983
+ }
35984
+
35985
+ // src/sinks/adbSink.ts
35986
+ var ADB_TIMEOUT_MS = 15000;
35987
+ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
35988
+ var FOCUSED_PACKAGE = /mCurrentFocus=Window\{\S+ \S+ ([^/\s}]+)/;
35989
+ function adbInputCommand(value) {
35990
+ if (!PRINTABLE_ASCII.test(value)) {
35991
+ throw new ClientError("adb input text types printable ASCII only; this value has other characters. The user has to enter it.");
35992
+ }
35993
+ if (value.includes("%s")) {
35994
+ throw new ClientError("adb input text reads %s as a space, so this value cannot be typed exactly. The user has to enter it.");
35995
+ }
35996
+ const quoted = value.replaceAll(" ", "%s").replaceAll("'", `'\\''`);
35997
+ return `input text '${quoted}'
35998
+ `;
35999
+ }
36000
+
36001
+ class AdbSink {
36002
+ resolver;
36003
+ adb;
36004
+ constructor(resolver, adb = "adb") {
36005
+ this.resolver = resolver;
36006
+ this.adb = adb;
36007
+ }
36008
+ async input(args) {
36009
+ if (refsIn(args.text).length === 0) {
36010
+ throw new ClientError("text needs a secret handle: {{secret://<credential_id>/<field>}}");
36011
+ }
36012
+ const target = args.serial === undefined ? [] : ["-s", args.serial];
36013
+ const secrets = await this.resolver.resolve([args.text]);
36014
+ const value = secrets.fill(args.text);
36015
+ const command = adbInputCommand(value);
36016
+ await this.requireFocus(target, args.expect_package);
36017
+ await this.shell(target, command, secrets);
36018
+ return {
36019
+ entered_length: value.length,
36020
+ package: args.expect_package,
36021
+ used: secrets.used()
36022
+ };
36023
+ }
36024
+ async requireFocus(target, expected) {
36025
+ const outcome = await runProcess({
36026
+ argv: [this.adb, ...target, "shell", "dumpsys", "window"],
36027
+ timeoutMs: ADB_TIMEOUT_MS
36028
+ });
36029
+ if (outcome.exitCode !== 0) {
36030
+ throw new ClientError(`adb could not read the focused window: ${outcome.stderr.trim()}`);
36031
+ }
36032
+ const focused = FOCUSED_PACKAGE.exec(outcome.stdout)?.[1];
36033
+ if (focused === undefined) {
36034
+ throw new ClientError("No focused app found on the device (is it locked?); nothing was typed.");
36035
+ }
36036
+ if (focused !== expected) {
36037
+ throw new ClientError(`The focused app is ${focused}, not ${expected}; nothing was typed.`);
36038
+ }
36039
+ }
36040
+ async shell(target, command, secrets) {
36041
+ const outcome = await runProcess({
36042
+ argv: [this.adb, ...target, "shell", "-T"],
36043
+ stdin: command,
36044
+ timeoutMs: ADB_TIMEOUT_MS
36045
+ });
36046
+ if (outcome.exitCode !== 0 || outcome.timedOut) {
36047
+ throw new ClientError(`adb input text failed (exit ${outcome.exitCode}${outcome.timedOut ? ", timed out" : ""}): ${secrets.redact(`${outcome.stderr}${outcome.stdout}`).trim()}`);
36048
+ }
36049
+ }
36050
+ }
36051
+
36052
+ // src/sinks/execSink.ts
36053
+ class ExecSink {
36054
+ resolver;
36055
+ constructor(resolver) {
36056
+ this.resolver = resolver;
36057
+ }
36058
+ async run(args) {
36059
+ if (args.argv.some(hasHandle)) {
36060
+ throw new ClientError("A secret handle cannot go in argv, which other processes can read: pass it in env or stdin.");
36061
+ }
36062
+ const env = args.env ?? {};
36063
+ const secrets = await this.resolver.resolve([
36064
+ ...Object.values(env),
36065
+ args.stdin ?? ""
36066
+ ]);
36067
+ if (secrets.used().length === 0) {
36068
+ throw new ClientError("secret_exec needs a secret handle in env or stdin; run other programs without it.");
36069
+ }
36070
+ const outcome = await runProcess({
36071
+ argv: args.argv,
36072
+ cwd: args.cwd,
36073
+ env: Object.fromEntries(Object.entries(env).map(([name, value]) => [name, secrets.fill(value)])),
36074
+ stdin: args.stdin === undefined ? undefined : secrets.fill(args.stdin),
36075
+ timeoutMs: args.timeout_s * 1000
36076
+ });
36077
+ const stdout = secrets.shown(outcome.stdout, outcome.stdoutCut);
36078
+ const stderr = secrets.shown(outcome.stderr, outcome.stderrCut);
36079
+ return {
36080
+ exit_code: outcome.exitCode,
36081
+ signal: outcome.signal,
36082
+ timed_out: outcome.timedOut,
36083
+ stdout: stdout.text,
36084
+ stderr: stderr.text,
36085
+ output_truncated: stdout.truncated || stderr.truncated,
36086
+ used: secrets.used()
36087
+ };
36088
+ }
36089
+ }
36090
+
36091
+ // src/sinks/httpSink.ts
36092
+ var MAX_READ_BYTES = 1024 * 1024;
36093
+ async function cappedText(response) {
36094
+ const reader = response.body?.getReader();
36095
+ if (reader === undefined)
36096
+ return { text: "", cut: false };
36097
+ const chunks = [];
36098
+ let size = 0;
36099
+ while (size < MAX_READ_BYTES) {
36100
+ const { done, value } = await reader.read();
36101
+ if (done) {
36102
+ return { text: Buffer.concat(chunks).toString("utf8"), cut: false };
36103
+ }
36104
+ chunks.push(value);
36105
+ size += value.length;
36106
+ }
36107
+ await reader.cancel();
36108
+ return {
36109
+ text: Buffer.concat(chunks).subarray(0, MAX_READ_BYTES).toString("utf8"),
36110
+ cut: true
36111
+ };
36112
+ }
36113
+ function causeOf(error48) {
36114
+ if (!(error48 instanceof Error))
36115
+ return String(error48);
36116
+ const cause = error48.cause instanceof Error ? `: ${error48.cause.message}` : "";
36117
+ return `${error48.message}${cause}`;
36118
+ }
36119
+
36120
+ class HttpSink {
36121
+ resolver;
36122
+ constructor(resolver) {
36123
+ this.resolver = resolver;
36124
+ }
36125
+ async send(args) {
36126
+ requireHandlesAfterOrigin(args.url);
36127
+ const headers = args.headers ?? {};
36128
+ const secrets = await this.resolver.resolve([
36129
+ args.url,
36130
+ ...Object.keys(headers),
36131
+ ...Object.values(headers),
36132
+ args.body ?? ""
36133
+ ]);
36134
+ const { response, text: text2, cut } = await this.exchange(args, headers, secrets);
36135
+ const body = secrets.shown(text2, cut);
36136
+ return {
36137
+ status: response.status,
36138
+ headers: Object.fromEntries([...response.headers].map(([name, value]) => [
36139
+ name,
36140
+ secrets.redact(value)
36141
+ ])),
36142
+ body: body.text,
36143
+ body_truncated: body.truncated,
36144
+ used: secrets.used()
36145
+ };
36146
+ }
36147
+ async exchange(args, headers, secrets) {
36148
+ try {
36149
+ const response = await fetch(secrets.fill(args.url, encodeURIComponent), {
36150
+ method: args.method,
36151
+ headers: Object.fromEntries(Object.entries(headers).map(([name, value]) => [
36152
+ secrets.fill(name),
36153
+ secrets.fill(value)
36154
+ ])),
36155
+ body: args.body === undefined ? undefined : secrets.fill(args.body),
36156
+ redirect: "manual",
36157
+ signal: AbortSignal.timeout(args.timeout_s * 1000)
36158
+ });
36159
+ return { response, ...await cappedText(response) };
36160
+ } catch (error48) {
36161
+ throw new ClientError(`The request failed: ${secrets.redact(causeOf(error48))}`);
36162
+ }
36163
+ }
36164
+ }
36165
+
35707
36166
  // src/tools/attachmentTools.ts
35708
36167
  function attachmentTools(attachments) {
35709
36168
  return [
@@ -35750,12 +36209,17 @@ var sealedFields = {
35750
36209
  };
35751
36210
  function credentialTools(credentials, migration, names) {
35752
36211
  return [
35753
- defineTool("reveal_sealed_credential", `Decrypt one sealed field of a sealed credential on this machine. The server only sees that the ciphertext was fetched. Find credentials and their visible fields with the standard credential tools (search_credentials, get_credential). ${EXPOSURE_NOTE}`, {
36212
+ defineTool("reveal_sealed_credential", `Decrypt one sealed field of a sealed credential on this machine. The server only sees that the ciphertext was fetched. Find credentials and their visible fields with the standard credential tools (search_credentials, get_credential). Refused unless its llm_visibility is visible. ${EXPOSURE_NOTE}`, {
35754
36213
  credential_id: credentialId,
35755
36214
  field: exports_external.string().min(1).describe("password, totp, sealed_notes, or custom:{key}.")
35756
36215
  }, (args) => credentials.reveal(args.credential_id, args.field)),
35757
- defineTool("get_sealed_totp", `Compute the current TOTP code of a sealed credential on this machine, without revealing the seed. ${EXPOSURE_NOTE}`, { credential_id: credentialId }, (args) => credentials.totp(args.credential_id)),
35758
- defineTool("insert_sealed_credential", `Create a sealed credential in a seal root. It is kept in the space of that root. password, totp, sealed_notes and the values of sealed custom fields are encrypted on this machine; title, username, url, tags, notes, visible custom fields and the key name of every custom field (sealed ones too) stay readable by the server so search and duplicate detection work. ${SECRET_ARGUMENT_NOTE}`, {
36216
+ defineTool("get_sealed_totp", `Compute the current TOTP code of a sealed credential on this machine, without revealing the seed. Refused unless its llm_visibility is visible. ${EXPOSURE_NOTE}`, { credential_id: credentialId }, (args) => credentials.totp(args.credential_id)),
36217
+ defineTool("set_sealed_credential_llm_visibility", "Make a sealed credential's secrets, or one sealed field, less visible to LLMs: use_only (never revealed to an LLM) or human_only (never revealed, entered by a person). It only tightens: human_only cannot go back to use_only from here, and only the user makes anything visible, in the SafeKB app. The value is not signed and does not change the credential's version.", {
36218
+ credential_id: credentialId,
36219
+ llm_visibility: exports_external.enum(["use_only", "human_only"]).describe("human_only is the more restrictive."),
36220
+ field: exports_external.string().min(1).describe("One sealed field: password, totp, sealed_notes or custom:{key}. Omit for the whole credential; a field without its own value follows the credential.").optional()
36221
+ }, (args) => credentials.setLlmVisibility(args.credential_id, args.llm_visibility, args.field)),
36222
+ defineTool("insert_sealed_credential", `Create a sealed credential in a seal root. It is kept in the space of that root. Whether an LLM may reveal its secrets follows the default the user chose for the tenant; only the user changes it, in the SafeKB app. password, totp, sealed_notes and the values of sealed custom fields are encrypted on this machine; title, username, url, tags, notes, visible custom fields and the key name of every custom field (sealed ones too) stay readable by the server so search and duplicate detection work. ${SECRET_ARGUMENT_NOTE}`, {
35759
36223
  root_id: rootRef("The seal root that will own it."),
35760
36224
  title: exports_external.string().min(1).describe("Visible to the server."),
35761
36225
  folder_id: exports_external.string().min(1).describe("Group it belongs to.").optional(),
@@ -35893,6 +36357,34 @@ function documentTools(creator, documents, search, names) {
35893
36357
  ];
35894
36358
  }
35895
36359
 
36360
+ // src/tools/sinkTools.ts
36361
+ var HANDLE_NOTE = "Name a secret by its handle {{secret://<credential_id>/<field>}} (field: password, totp, sealed_notes or custom:{key}); it is decrypted on this machine and the value never enters the conversation. A totp handle becomes the current code. Fields whose llm_visibility is human_only are refused.";
36362
+ var SCRUB_NOTE = "What comes back is scrubbed of the values (as is, URL-encoded, base64, hex, JSON-escaped) and names the secrets used. Scrubbing guards against careless output, not against a program written to leak: run only code you trust.";
36363
+ var timeout = (fallback) => exports_external.number().int().min(1).max(600).default(fallback).describe("Seconds.");
36364
+ function sinkTools(exec, http, adb) {
36365
+ return [
36366
+ defineTool("secret_exec", `Run a program with secrets in its environment or on its stdin, without seeing them. ${HANDLE_NOTE} At least one handle is needed; handles are refused in argv, which other processes can read. The program inherits this client's environment. No shell: argv is run as is. ${SCRUB_NOTE}`, {
36367
+ argv: exports_external.array(exports_external.string()).min(1).describe('Program and arguments, e.g. ["uv", "run", "script.py"].'),
36368
+ cwd: absolutePath("Working directory."),
36369
+ env: exports_external.record(exports_external.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), exports_external.string()).describe('Added to the environment; values may hold handles, e.g. {"API_KEY": "{{secret://<id>/password}}"}.').optional(),
36370
+ stdin: exports_external.string().describe("Written to stdin, then closed; may hold handles.").optional(),
36371
+ timeout_s: timeout(60)
36372
+ }, (args) => exec.run(args)),
36373
+ defineTool("secret_http", `Send one HTTP request with secrets in its url, headers or body, without seeing them. ${HANDLE_NOTE} Handles go in the path, query or fragment of the url (URL-encoded for you), never its host. A redirect is returned, not followed. ${SCRUB_NOTE}`, {
36374
+ method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]),
36375
+ url: exports_external.string().url().describe("May hold handles, e.g. in the query."),
36376
+ headers: exports_external.record(exports_external.string(), exports_external.string()).describe('e.g. {"Authorization": "Bearer {{secret://<id>/password}}"}.').optional(),
36377
+ body: exports_external.string().describe("May hold handles.").optional(),
36378
+ timeout_s: timeout(30)
36379
+ }, (args) => http.send(args)),
36380
+ defineTool("secret_adb_input", `Type a secret into the focused field of an Android device with adb (input text), without seeing it. ${HANDLE_NOTE} Only printable ASCII can be typed; anything else is refused, never typed differently. Returns only the length typed.`, {
36381
+ text: exports_external.string().min(1).describe("What to type, holding a handle: {{secret://<id>/password}}."),
36382
+ serial: exports_external.string().min(1).describe("adb -s: which device, when several are connected.").optional(),
36383
+ expect_package: exports_external.string().min(1).describe("Type only if this app has the focus, e.g. com.example.bank.")
36384
+ }, (args) => adb.input(args))
36385
+ ];
36386
+ }
36387
+
35896
36388
  // src/tools/treeTools.ts
35897
36389
  var documentId = objectId2("Id of the sealed document or folder.");
35898
36390
  function treeTools(tree, names) {
@@ -35946,6 +36438,8 @@ The keys of this machine, and backups, are handled by tools that never take or r
35946
36438
  A credential with \`sealed: true\` belongs to a seal root. Its title, username, url, tags, notes and visible custom fields are readable by the server: find it with the standard credential tools. Its password, TOTP seed, sealed_notes and sealed custom fields open only here: reveal_sealed_credential, get_sealed_totp, insert_sealed_credential, update_sealed_credential.
35947
36439
 
35948
36440
  - Reveal a secret only when the task needs it, and prefer get_sealed_totp over revealing a TOTP seed.
36441
+ - llm_visibility is set per credential, and llm_field_visibility overrides it for single sealed fields. reveal_sealed_credential and get_sealed_totp work only on a field that is visible. Only the user makes anything visible, in the SafeKB app.
36442
+ - To use a secret without seeing it (visible or use_only fields): secret_exec (a program gets it in env or stdin), secret_http (one request gets it in url, headers or body) or secret_adb_input (typed on an Android device). Name it by handle, {{secret://<credential_id>/<field>}}; a totp handle becomes the current code. Prefer these over revealing, even for a visible field. A human_only field is never used by a tool: ask the user to enter it.
35949
36443
  - visible_tampered means the visible fields were changed without the key since the last sealed save. Do not trust the url or username of such a credential until the user has reviewed the change.`;
35950
36444
  function createSealedTools(ctx) {
35951
36445
  const services = createSealedServices(ctx);
@@ -35956,7 +36450,8 @@ function createSealedTools(ctx) {
35956
36450
  ...treeTools(services.tree, names),
35957
36451
  ...attachmentTools(services.attachments),
35958
36452
  ...depositTools(services.deposits, names),
35959
- ...credentialTools(services.credentials, services.migration, names)
36453
+ ...credentialTools(services.credentials, services.migration, names),
36454
+ ...sinkTools(new ExecSink(services.secrets), new HttpSink(services.secrets), new AdbSink(services.secrets))
35960
36455
  ];
35961
36456
  }
35962
36457
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safekb/mcp",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Mustafa Öztürk",