agentlas 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.5 — 2026-07-27
4
+
5
+ - A verifier that reports "no issues" as `[""]` instead of `[]` no longer
6
+ fails the run on a contract error. An empty string is a mis-spelling of
7
+ absence, not content, so it is normalized away before the issue contract
8
+ is checked — a passing verification stopped the last step of a real run
9
+ this way.
10
+
11
+ ## 1.0.4 — 2026-07-27
12
+
13
+ **The hub boundary stops guessing.** Owner decision after live runs: rules
14
+ that infer private data from shape were removed rather than tuned, because
15
+ each one refused work orders whose flagged phrase *was* the task — a slash
16
+ between Korean words read as a file path, "멱등키 설계" read as a credential
17
+ assignment, "고객 ID를 조회하는 API" read as a labeled identifier. No repair
18
+ was possible, so the request simply died.
19
+
20
+ - Gone: path inference from a slash, labeled-identifier inference, UUID/IP/
21
+ phone shape matching, keyword-adjacency credential matching.
22
+ - Kept: forms that can only be one thing — issuer-prefixed provider tokens,
23
+ PEM headers, JWTs, credentials embedded in a URL, email addresses.
24
+ - Those are now **redacted from the outgoing text instead of refusing the
25
+ run**, and the redaction is printed, so a pasted secret never leaves the
26
+ machine and the task still proceeds. `AGENTLAS_HUB_BOUNDARY=off` sends
27
+ text verbatim.
28
+ - Upload and packaging are unchanged: they already block by file identity
29
+ (`.env*`, `*.pem/key/p12`, `credentials*`, `id_rsa`), which is fact rather
30
+ than inference.
31
+
32
+ Shared fixtures (`privacy-guard-fixtures/`) and `scripts/sync-privacy-guard.sh`
33
+ now pin this contract across the terminal engine and the server.
34
+
3
35
  ## 1.0.3 — 2026-07-27
4
36
 
5
37
  Live `workforce` runs surfaced four defects that no unit gate could reach.
@@ -806,49 +806,79 @@ function decodedHubText(value) {
806
806
  return text;
807
807
  }
808
808
 
809
+ /*
810
+ * Hub-bound text hygiene — SELF-PROVING FORMS ONLY, and it masks instead of killing.
811
+ *
812
+ * Owner decision 2026-07-27, after live runs: every GUESSING rule (a slash after
813
+ * a word = a path, "key"/"secret" near a value = a credential, digit runs =
814
+ * a phone, hex groups = a UUID/IP) produced false positives that killed real
815
+ * requests with no repair available — the flagged phrase WAS the task
816
+ * ("진단/멱등키", "멱등키 설계"). Guessing is removed, not tuned.
817
+ *
818
+ * What remains is a narrow certainty: strings whose form can only be one thing
819
+ * (provider token with its issuer prefix, PEM header, JWT, credentials in a
820
+ * URL, an email address). Those are redacted from the outgoing text rather than
821
+ * refused, so a paste accident never leaves the machine and the run continues.
822
+ * Set AGENTLAS_HUB_BOUNDARY=off to send text verbatim.
823
+ */
824
+ const HUB_REDACTION_PATTERNS = [
825
+ ["secret_provider_token", /\b(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g],
826
+ ["secret_private_key", /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/gi],
827
+ ["secret_bearer_token", /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi],
828
+ ["secret_jwt", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g],
829
+ ["secret_credential_url", /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@[^\s/]+/gi],
830
+ ["email", /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi],
831
+ ];
832
+
833
+ function hubBoundaryEnabled(env = process.env) {
834
+ return String(env.AGENTLAS_HUB_BOUNDARY || "").trim().toLowerCase() !== "off";
835
+ }
836
+
837
+ /** Redact self-proving secrets/identifiers. Returns { text, redacted: [kinds] }. */
838
+ function redactHubText(value) {
839
+ if (!hubBoundaryEnabled()) return { text: value, redacted: [] };
840
+ let text = String(value);
841
+ const redacted = [];
842
+ for (const [kind, pattern] of HUB_REDACTION_PATTERNS) {
843
+ pattern.lastIndex = 0;
844
+ if (pattern.test(text)) {
845
+ pattern.lastIndex = 0;
846
+ text = text.replace(pattern, "<redacted>");
847
+ redacted.push(kind);
848
+ }
849
+ }
850
+ return { text, redacted };
851
+ }
852
+
809
853
  function hubTextFindingKinds(value) {
810
- const text = decodedHubText(value);
811
- const findings = [];
812
- if (HUB_EMAIL_RE.test(text)) findings.push("email");
813
- HUB_UUID_RE.lastIndex = 0;
814
- const hasUuid = HUB_UUID_RE.test(text);
815
- HUB_UUID_RE.lastIndex = 0;
816
- const phoneText = text.replace(HUB_UUID_RE, " ");
817
- HUB_PHONE_RE.lastIndex = 0;
818
- for (const match of phoneText.matchAll(HUB_PHONE_RE)) {
819
- const digits = (match[0].match(/\d/g) || []).length;
820
- if (digits >= 10 && digits <= 15) { findings.push("phone"); break; }
821
- }
822
- if (HUB_LABELED_ID_RE.test(text)) findings.push("labeled_identifier");
823
- if (hasUuid) findings.push("uuid");
824
- HUB_IP_RE.lastIndex = 0;
825
- for (const match of text.matchAll(HUB_IP_RE)) {
826
- if (net.isIP(match[0].replace(/^\[|\]$/g, ""))) { findings.push("ip_address"); break; }
827
- }
828
- const masked = text.replace(HUB_HTTPS_RE, " ").replace(HUB_PLACEHOLDER_RE, " ");
829
- if (HUB_PATH_PATTERNS.some((pattern) => pattern.test(masked))) findings.push("local_path");
830
- for (const [kind, pattern] of HUB_SECRET_PATTERNS) if (pattern.test(text)) findings.push(`secret_${kind}`);
831
- return [...new Set(findings)];
832
- }
833
-
834
- function assertHubWorkOrderBoundary(order) {
835
- const issues = [];
836
- const fields = [["taskBrief", order.taskBrief]];
854
+ // 인코딩 우회로 숨긴 자격증명도 같은 기준으로 본다(판정만, 치환은 원문 기준).
855
+ return redactHubText(decodedHubText(value)).redacted;
856
+ }
857
+
858
+ /**
859
+ * Redact self-proving secrets from the Hub-bound WorkOrder in place.
860
+ *
861
+ * This replaced a reject-and-ask-the-model-to-rewrite gate. Refusing was the
862
+ * wrong shape twice over: the model cannot remove a phrase that IS the task, and
863
+ * a real pasted credential should never depend on a model choosing to drop it.
864
+ * Redaction is deterministic, keeps the run alive, and the caller reports what
865
+ * was masked so nothing is hidden from the user.
866
+ */
867
+ function redactHubWorkOrder(order) {
868
+ const redactions = [];
869
+ const applyField = (fieldPath, value, assign) => {
870
+ if (typeof value !== "string" || !value) return;
871
+ const { text, redacted } = redactHubText(value);
872
+ if (!redacted.length) return;
873
+ assign(text);
874
+ for (const kind of redacted) redactions.push({ path: fieldPath, kind });
875
+ };
876
+ applyField("taskBrief", order.taskBrief, (next) => { order.taskBrief = next; });
837
877
  order.roleSlots.forEach((slot, index) => {
838
- fields.push([`roleSlots[${index}].title`, slot.title], [`roleSlots[${index}].task`, slot.task]);
878
+ applyField(`roleSlots[${index}].title`, slot.title, (next) => { slot.title = next; });
879
+ applyField(`roleSlots[${index}].task`, slot.task, (next) => { slot.task = next; });
839
880
  });
840
- for (const [fieldPath, value] of fields) {
841
- for (const kind of hubTextFindingKinds(value)) {
842
- issues.push({ path: fieldPath, code: kind.startsWith("secret_") ? `hub_${kind}` : `hub_private_${kind}` });
843
- }
844
- }
845
- if (issues.length) {
846
- fail(
847
- "work_order_hub_boundary_rejected",
848
- "Hub-bound WorkOrder free text failed the deterministic privacy boundary",
849
- { issues },
850
- );
851
- }
881
+ return redactions;
852
882
  }
853
883
 
854
884
  function validateWorkOrder(value) {
@@ -923,7 +953,12 @@ function validateWorkOrder(value) {
923
953
  if (!Number.isInteger(policy.minimumCandidatesPerSlot) || policy.minimumCandidatesPerSlot < 2 || policy.minimumCandidatesPerSlot > 30) fail("work_order_invalid", "selectionPolicy.minimumCandidatesPerSlot is invalid");
924
954
  if (!Number.isInteger(policy.maximumCandidatesPerSlot) || policy.maximumCandidatesPerSlot < 2 || policy.maximumCandidatesPerSlot > 100) fail("work_order_invalid", "selectionPolicy.maximumCandidatesPerSlot is invalid");
925
955
  if (policy.minimumCandidatesPerSlot > policy.maximumCandidatesPerSlot) fail("work_order_invalid", "candidate window minimum exceeds maximum");
926
- assertHubWorkOrderBoundary(order);
956
+ // 자기증명 자격증명만 결정적으로 마스킹한다(거절 아님) — 마스킹 내역은
957
+ // 비파괴 필드로 달아 호출자가 화면에 정직히 표시한다.
958
+ const hubRedactions = redactHubWorkOrder(order);
959
+ if (hubRedactions.length) {
960
+ Object.defineProperty(order, "__hubRedactions", { value: hubRedactions, enumerable: false });
961
+ }
927
962
  return order;
928
963
  }
929
964
 
@@ -1452,7 +1487,12 @@ function validateVerifierResult(value) {
1452
1487
  if (!["passed", "failed"].includes(check.status)) fail("verifier_invalid", "verifier check status is invalid");
1453
1488
  assertString(check.evidence, "verifier.evidence", 2_000);
1454
1489
  }
1455
- assertArray(result.issues, "verifier.issues", 64).forEach((item, index) => assertString(item, `verifier.issues[${index}]`, 2_000));
1490
+ // 모델은 "지적 없음" []가 아니라 [""]로 쓰기도 한다(합격 판정 실측). 빈 문자열은
1491
+ // 내용이 아니라 부재의 오표기이므로 정규화해서 버린다 — 남은 항목만 계약 검사.
1492
+ const issues = assertArray(result.issues, "verifier.issues", 64)
1493
+ .filter((item) => !(typeof item === "string" && !item.trim()));
1494
+ issues.forEach((item, index) => assertString(item, `verifier.issues[${index}]`, 2_000));
1495
+ result.issues = issues;
1456
1496
  return result;
1457
1497
  }
1458
1498
 
@@ -2712,6 +2752,12 @@ function create(deps = {}) {
2712
2752
  // 실황 내레이션: 사용자는 "누가 소집됐고 지금 뭘 하는지"를 보면서 신뢰를
2713
2753
  // 형성한다(2026-07-27 오너 요구). 결과에 영향 없는 표시 전용 — silent 존중.
2714
2754
  if (!ctx.silent) {
2755
+ const redactions = workOrder.__hubRedactions || [];
2756
+ if (redactions.length) {
2757
+ ui.info(ui.lang === "ko"
2758
+ ? `허브 전송 전 ${redactions.length}건 마스킹: ${[...new Set(redactions.map((row) => row.kind))].join(", ")}`
2759
+ : `redacted before leaving this machine (${redactions.length}): ${[...new Set(redactions.map((row) => row.kind))].join(", ")}`);
2760
+ }
2715
2761
  const nameByRelease = new Map();
2716
2762
  for (const slotRow of candidateSet.slots) {
2717
2763
  for (const cand of slotRow.candidates) nameByRelease.set(cand.agentReleaseId, cand.name || cand.agentReleaseId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"