hippo-memory 1.61.0 → 1.63.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 +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +62 -66
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/promote.js +55 -66
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +2 -2
- package/dist/capture/compact.js +11 -15
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +80 -76
- package/dist/cli/transfer.js +88 -107
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -2
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +103 -91
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +46 -39
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +96 -81
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +34 -56
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/secret-detect.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret detection for memory content (
|
|
3
|
-
* docs/plans/2026-07-01-memory-scope-isolation.md).
|
|
2
|
+
* Secret detection for memory content (memory scope isolation).
|
|
4
3
|
*
|
|
5
4
|
* Conservative, provider-bounded patterns only - a bare `sk-` in prose must
|
|
6
5
|
* NOT flag. Detection gates two surfaces:
|
|
@@ -11,7 +10,7 @@
|
|
|
11
10
|
* project origin. Explicit recall is unaffected - recalling a secret is
|
|
12
11
|
* a deliberate act.
|
|
13
12
|
*
|
|
14
|
-
* This is deliberately a thin slice of
|
|
13
|
+
* This is deliberately a thin slice of lifecycle compliance
|
|
15
14
|
* (no PII detection). Write-time scrubbing covers only text no person
|
|
16
15
|
* typed into hippo; see `vetSecrets`.
|
|
17
16
|
*
|
|
@@ -48,11 +47,8 @@ const SECRET_PATTERNS = [
|
|
|
48
47
|
// Generic assignment: api_key=..., password: ..., token = "..." where the
|
|
49
48
|
// value actually LOOKS like a credential: 12+ chars drawn only from
|
|
50
49
|
// token-safe characters AND containing at least one digit. Without both
|
|
51
|
-
// constraints this pattern flags ordinary code snippets and prose
|
|
52
|
-
// `token = estimateTokens(
|
|
53
|
-
// rollout worked well" were verified false positives that would silently
|
|
54
|
-
// hide real code-lesson memories from ambient context (post-merge
|
|
55
|
-
// adversarial review, 2026-07-02).
|
|
50
|
+
// constraints this pattern flags ordinary code snippets and prose
|
|
51
|
+
// (`token = estimateTokens(...)`), silently hiding code-lesson memories from ambient context.
|
|
56
52
|
{ name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_\-+/]*\d)[A-Za-z0-9_\-+/=]{12,}/i },
|
|
57
53
|
];
|
|
58
54
|
const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
|
|
@@ -86,8 +82,8 @@ export function detectSecret(entry) {
|
|
|
86
82
|
* Replace secret-shaped substrings in free text with a redaction marker.
|
|
87
83
|
* Reuses the same `SECRET_PATTERNS` / co-occurrence guard as `detectSecret`
|
|
88
84
|
* (which only flags whole-entry content) so callers that must persist raw
|
|
89
|
-
* text that never passes through the normal capture content gate
|
|
90
|
-
*
|
|
85
|
+
* text that never passes through the normal capture content gate (e.g. the
|
|
86
|
+
* pre-compact snapshot fields) can scrub it in place instead.
|
|
91
87
|
*/
|
|
92
88
|
export function redactSecrets(text) {
|
|
93
89
|
return redactText(text, false);
|
package/dist/server/auth.js
CHANGED
|
@@ -3,9 +3,9 @@ import { envRequireAuth } from '../env.js';
|
|
|
3
3
|
import { resolveTenantId } from '../tenant.js';
|
|
4
4
|
import { log } from '../log.js';
|
|
5
5
|
import { API_KEY_PREFIX, verifyApiKeyCached } from '../auth.js';
|
|
6
|
-
import { HttpError, isCrossSite, isHeaderString, LOOPBACK_HOST_HEADER } from '../http-util.js';
|
|
6
|
+
import { HttpError, isCrossSite, isHeaderString, LOOPBACK_HOST_HEADER, MAX_ID_LEN } from '../http-util.js';
|
|
7
7
|
import { requestIds } from './request.js';
|
|
8
|
-
import { isJsonString } from '
|
|
8
|
+
import { isJsonString } from '../json.js';
|
|
9
9
|
/**
|
|
10
10
|
* Recognise loopback remote addresses. Node reports IPv6-mapped IPv4 as
|
|
11
11
|
* '::ffff:127.0.0.1' on dual-stack sockets, so we accept that alongside
|
|
@@ -86,9 +86,9 @@ function sanitiseResolved(r) {
|
|
|
86
86
|
return null;
|
|
87
87
|
// Core reserves `__`-prefixed tenants (`__host__`, `__unroutable__`).
|
|
88
88
|
const tenant = tenantId.trim();
|
|
89
|
-
if (tenant.startsWith('__') || tenant.length >
|
|
89
|
+
if (tenant.startsWith('__') || tenant.length > MAX_ID_LEN || hasControlChar(tenant))
|
|
90
90
|
return null;
|
|
91
|
-
if (!isJsonString(subject) || subject.length < 1 || subject.length >
|
|
91
|
+
if (!isJsonString(subject) || subject.length < 1 || subject.length > MAX_ID_LEN)
|
|
92
92
|
return null;
|
|
93
93
|
// Padding would let "system " pass the reserved-name check yet read as `system` in an audit log.
|
|
94
94
|
if (hasControlChar(subject) || subject !== subject.trim())
|
|
@@ -174,7 +174,7 @@ export async function buildContextWithAuth(req, opts) {
|
|
|
174
174
|
throw new HttpError(401, 'auth required');
|
|
175
175
|
}
|
|
176
176
|
assertLocalCaller(req);
|
|
177
|
-
//
|
|
177
|
+
// Loopback fallback is process-local, treat as admin.
|
|
178
178
|
return {
|
|
179
179
|
hippoRoot: opts.hippoRoot,
|
|
180
180
|
tenantId: resolveTenantId({}),
|
package/dist/server/client-ip.js
CHANGED
|
@@ -73,7 +73,7 @@ function isTrustedProxy(list, ip) {
|
|
|
73
73
|
return family !== 0 && list.check(bare, family === 6 ? 'ipv6' : 'ipv4');
|
|
74
74
|
}
|
|
75
75
|
export function enforceRateLimit(req, path, limiter) {
|
|
76
|
-
//
|
|
76
|
+
// Per-IP rate limit on /v1/* and /mcp* to bound api-key-id enumeration. /health
|
|
77
77
|
// (a liveness probe) and other paths are never throttled. A 429 thrown
|
|
78
78
|
// here lands in the createServer catch like any other HttpError.
|
|
79
79
|
//
|
package/dist/server/cursor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpError } from '../http-util.js';
|
|
2
|
-
import { isJsonNumber
|
|
2
|
+
import { isJsonNumber } from './validation.js';
|
|
3
|
+
import { isJsonString } from '../json.js';
|
|
3
4
|
/** Bare-array routes cannot grow a body field without breaking clients, so their next cursor rides this header. */
|
|
4
5
|
export const NEXT_CURSOR_HEADER = 'X-Next-Cursor';
|
|
5
6
|
const MAX_CURSOR_CHARS = 1024;
|
package/dist/server/mcp-http.js
CHANGED
|
@@ -6,7 +6,7 @@ import { HttpError, isJsonObjectRecord, readBody, sendJson } from '../http-util.
|
|
|
6
6
|
import { buildContextWithAuth, heartbeatVerdict, readAuthHeader, requireAuth } from './auth.js';
|
|
7
7
|
import { clientIpForRateLimit } from './client-ip.js';
|
|
8
8
|
import { requestIds } from './request.js';
|
|
9
|
-
import { isJsonString } from '
|
|
9
|
+
import { isJsonString } from '../json.js';
|
|
10
10
|
/**
|
|
11
11
|
* Build a per-client key for MCP state isolation under HTTP-MCP. Used by
|
|
12
12
|
* mcp/server.ts to scope `lastRecalledIds` to the calling client so two
|
|
@@ -26,7 +26,7 @@ function buildMcpClientKey(req) {
|
|
|
26
26
|
const addr = req.socket.remoteAddress ?? 'unknown';
|
|
27
27
|
return `http:${tokenHash}:${addr}`;
|
|
28
28
|
}
|
|
29
|
-
// ── MCP-over-HTTP/SSE transport
|
|
29
|
+
// ── MCP-over-HTTP/SSE transport ──
|
|
30
30
|
//
|
|
31
31
|
// Two routes implement an MCP HTTP transport alongside the stdio one. Both
|
|
32
32
|
// dispatch to the same `handleMcpRequest` as the stdio loop in src/mcp/server.ts.
|
|
@@ -71,7 +71,7 @@ export async function handleMcpPost(req, res, opts) {
|
|
|
71
71
|
mcpRes = await handleMcpRequest(rpcReq, {
|
|
72
72
|
hippoRoot: ctx.hippoRoot,
|
|
73
73
|
tenantId: ctx.tenantId,
|
|
74
|
-
//
|
|
74
|
+
// McpContext.actor stays string; extract subject at the boundary.
|
|
75
75
|
actor: ctx.actor.subject,
|
|
76
76
|
// The caller's real role: MCP tools must not run a member key as admin.
|
|
77
77
|
role: ctx.actor.role,
|
|
@@ -131,7 +131,7 @@ export async function handleMcpStream(req, res, opts, streamSlots) {
|
|
|
131
131
|
// Initial ping so smoke tests can confirm the stream is live without
|
|
132
132
|
// waiting for the first keepalive interval.
|
|
133
133
|
res.write(': ping\n\n');
|
|
134
|
-
//
|
|
134
|
+
// SSE hardening:
|
|
135
135
|
// - Heartbeat re-validates the bearer (default 60s). If the key was
|
|
136
136
|
// revoked or rotated, close the stream with reason='auth_revoked'.
|
|
137
137
|
// - MCP_SSE_MAX_AGE_SEC (default 3600) caps stream lifetime; close
|
package/dist/server/request.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
/** The caller's `X-Request-Id` when it is a short plain token, else a fresh UUID. */
|
|
3
3
|
export declare function resolveRequestId(header: string | string[] | undefined): string;
|
|
4
|
-
/** One line per failed request; 4xx is the caller's mistake
|
|
4
|
+
/** One line per failed request; 4xx is the caller's mistake and a busy 503 is back-pressure the client retries, so neither logs as an error. */
|
|
5
5
|
export declare function logRequestFailure<E>(req: IncomingMessage, err: E, requestId: string, status: number): void;
|
|
6
6
|
/** The status and client message for a failed request; a held write lock is a retryable 503, never a 500. */
|
|
7
7
|
export declare function replyFor<E>(err: E): {
|
|
@@ -22,11 +22,8 @@ export declare const requestIds: WeakMap<IncomingMessage, string>;
|
|
|
22
22
|
* them — otherwise `%2F` becomes `/`, path-split runs, and the route either
|
|
23
23
|
* silently 404s or matches the wrong template.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* the regex matched `%2F` anywhere in `req.url`. Recall queries containing
|
|
28
|
-
* URLs would have been rejected as bypass attempts. Splitting on the first
|
|
29
|
-
* `?` confines the check to the path.
|
|
25
|
+
* Only the PATHNAME is scanned (split on the first `?`), so recall queries
|
|
26
|
+
* containing URLs like `?q=https%3A%2F%2Fexample.com` are not rejected.
|
|
30
27
|
*/
|
|
31
28
|
export declare function rejectEncodedSlash(rawUrl: string): void;
|
|
32
29
|
export {};
|
package/dist/server/request.js
CHANGED
|
@@ -9,11 +9,13 @@ export function resolveRequestId(header) {
|
|
|
9
9
|
const value = Array.isArray(header) ? undefined : header;
|
|
10
10
|
return value && REQUEST_ID_RE.test(value) ? value : randomUUID();
|
|
11
11
|
}
|
|
12
|
-
/** One line per failed request; 4xx is the caller's mistake
|
|
12
|
+
/** One line per failed request; 4xx is the caller's mistake and a busy 503 is back-pressure the client retries, so neither logs as an error. */
|
|
13
13
|
export function logRequestFailure(req, err, requestId, status) {
|
|
14
14
|
const message = err instanceof Error ? err.message : String(err);
|
|
15
15
|
const line = `${req.method ?? 'GET'} ${(req.url ?? '/').split('?')[0]} failed: ${message}`;
|
|
16
|
-
if (
|
|
16
|
+
if (isSqliteBusy(err))
|
|
17
|
+
log.warn(line, { requestId, status });
|
|
18
|
+
else if (status >= 500)
|
|
17
19
|
log.error(line, { requestId, status, ...errorFields(err) });
|
|
18
20
|
else
|
|
19
21
|
log.info(line, { requestId, status });
|
|
@@ -87,11 +89,8 @@ export const requestIds = new WeakMap();
|
|
|
87
89
|
* them — otherwise `%2F` becomes `/`, path-split runs, and the route either
|
|
88
90
|
* silently 404s or matches the wrong template.
|
|
89
91
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* the regex matched `%2F` anywhere in `req.url`. Recall queries containing
|
|
93
|
-
* URLs would have been rejected as bypass attempts. Splitting on the first
|
|
94
|
-
* `?` confines the check to the path.
|
|
92
|
+
* Only the PATHNAME is scanned (split on the first `?`), so recall queries
|
|
93
|
+
* containing URLs like `?q=https%3A%2F%2Fexample.com` are not rejected.
|
|
95
94
|
*/
|
|
96
95
|
export function rejectEncodedSlash(rawUrl) {
|
|
97
96
|
const queryIdx = rawUrl.indexOf('?');
|
|
@@ -4,7 +4,8 @@ import { auditList, authCreate, authListRows, authRevoke, quarantineApprove, qua
|
|
|
4
4
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
5
5
|
import { assertCrossTenantAdmin, buildContextWithAuth } from '../auth.js';
|
|
6
6
|
import { pageOf, parseCursor, setNextCursorHeader } from '../cursor.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
|
|
8
|
+
import { isJsonString } from '../../json.js';
|
|
8
9
|
const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
|
|
9
10
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
10
11
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
@@ -13,7 +14,7 @@ const MAX_AUDIT_LIMIT = 10000;
|
|
|
13
14
|
// GET /v1/auth/keys had no limit, so its default page is the cap: a deployment below 1000 keys sees no change.
|
|
14
15
|
const MAX_AUTH_KEYS_PAGE = 1000;
|
|
15
16
|
// POST /v1/auth/keys — mint a new API key. Plaintext lands in the response
|
|
16
|
-
// body
|
|
17
|
+
// body: the HTTP layer hands it to the client; the user-facing
|
|
17
18
|
// "store this somewhere safe" warning belongs in the CLI client, not here.
|
|
18
19
|
export async function handleCreateAuthKey({ req, res, opts }) {
|
|
19
20
|
const ctx = await buildContextWithAuth(req, opts);
|
|
@@ -22,7 +23,7 @@ export async function handleCreateAuthKey({ req, res, opts }) {
|
|
|
22
23
|
if (labelRaw !== undefined && !isJsonString(labelRaw)) {
|
|
23
24
|
throw new HttpError(400, 'label must be a string');
|
|
24
25
|
}
|
|
25
|
-
//
|
|
26
|
+
// Optional body.role mirrors the --role CLI flag. Validated
|
|
26
27
|
// strictly — anything other than 'admin'|'member' is a 400 (no silent
|
|
27
28
|
// fallback to admin). authCreate refuses a member caller with a 403.
|
|
28
29
|
const roleRaw = body['role'];
|
|
@@ -76,7 +77,7 @@ export async function handleRevokeAuthKey({ req, res, opts }, keyMatch) {
|
|
|
76
77
|
sendJson(res, 200, result);
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
|
-
// GET /v1/quarantine?status=&limit=&cursor=:
|
|
80
|
+
// GET /v1/quarantine?status=&limit=&cursor=: quarantine review queue. quarantineList carries no role gate itself, so it's checked here.
|
|
80
81
|
export async function handleListQuarantine({ req, res, opts, query }) {
|
|
81
82
|
const ctx = await buildContextWithAuth(req, opts);
|
|
82
83
|
if (ctx.actor.role !== 'admin') {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// /v1/customer-notes routes.
|
|
2
|
-
import { closeCustomerNote, loadCustomerNoteById, loadCustomerNotes, saveCustomerNote, VALID_NOTE_STATES } from '../../customer-notes.js';
|
|
2
|
+
import { closeCustomerNote, loadCustomerNoteById, loadCustomerNotes, MAX_CUSTOMER_LEN, saveCustomerNote, VALID_NOTE_STATES } from '../../customer-notes.js';
|
|
3
3
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
4
4
|
import { buildContextWithAuth } from '../auth.js';
|
|
5
5
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
7
|
+
import { isJsonString } from '../../json.js';
|
|
8
|
+
// ── customer_note routes ──
|
|
8
9
|
//
|
|
9
10
|
// 5 routes (no assembler/refresh): POST /v1/customer-notes (new; body customer +
|
|
10
11
|
// note), GET /v1/customer-notes (list; status + customer filter; shared
|
|
@@ -19,8 +20,8 @@ export async function handleCreateCustomerNote({ req, res, opts }) {
|
|
|
19
20
|
if (!isJsonString(customer) || customer.trim().length === 0) {
|
|
20
21
|
throw new HttpError(400, 'customer is required (non-empty string)');
|
|
21
22
|
}
|
|
22
|
-
if (customer.length >
|
|
23
|
-
throw new HttpError(400,
|
|
23
|
+
if (customer.length > MAX_CUSTOMER_LEN) {
|
|
24
|
+
throw new HttpError(400, `customer exceeds ${MAX_CUSTOMER_LEN}-character cap`);
|
|
24
25
|
}
|
|
25
26
|
const note = body['note'];
|
|
26
27
|
if (!isJsonString(note) || note.trim().length === 0) {
|
|
@@ -4,15 +4,16 @@ import { HttpError, sendJson } from '../../http-util.js';
|
|
|
4
4
|
import { NotFoundError } from '../../api-errors.js';
|
|
5
5
|
import { buildContextWithAuth } from '../auth.js';
|
|
6
6
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
7
|
-
import { isJsonNumber,
|
|
8
|
-
|
|
7
|
+
import { isJsonNumber, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
8
|
+
import { isJsonString } from '../../json.js';
|
|
9
|
+
// ── decisions (first-class object) ──
|
|
9
10
|
//
|
|
10
11
|
// 5 routes: POST /v1/decisions (create, optional supersedesDecisionId),
|
|
11
12
|
// GET /v1/decisions (list, status filter), GET /v1/decisions/:id (show),
|
|
12
13
|
// POST /v1/decisions/:id/supersede (create a successor + supersede :id),
|
|
13
14
|
// POST /v1/decisions/:id/close (retire). Bearer-authed + tenant-scoped via
|
|
14
15
|
// buildContextWithAuth. status validated against VALID_DECISION_STATES.
|
|
15
|
-
// DoS caps: text 4096, context 4096
|
|
16
|
+
// DoS caps: text 4096, context 4096. The HTTP surface is
|
|
16
17
|
// new (no legacy --supersedes <memory-id> constraint), so it supersedes by
|
|
17
18
|
// table id and never weakens a memory mirror.
|
|
18
19
|
export async function handleCreateDecision({ req, res, opts }) {
|
|
@@ -4,8 +4,10 @@ import { HttpError, sendJson } from '../../http-util.js';
|
|
|
4
4
|
import { NotFoundError } from '../../api-errors.js';
|
|
5
5
|
import { buildContextWithAuth } from '../auth.js';
|
|
6
6
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
8
|
+
import { isJsonString } from '../../json.js';
|
|
9
|
+
const MAX_LINKED_MEMORY_IDS = 256;
|
|
10
|
+
// ── incidents (first-class object) ──
|
|
9
11
|
//
|
|
10
12
|
// 5 routes: POST /v1/incidents (open; body text + context + linkedMemoryIds[]),
|
|
11
13
|
// GET /v1/incidents (list, status filter), GET /v1/incidents/:id (show),
|
|
@@ -13,7 +15,7 @@ import { isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../val
|
|
|
13
15
|
// POST /v1/incidents/:id/close (open|resolved -> closed). Bearer-authed +
|
|
14
16
|
// tenant-scoped via buildContextWithAuth. status validated against
|
|
15
17
|
// VALID_INCIDENT_STATES. DoS caps: text 4096, context 4096, resolutionText
|
|
16
|
-
// 4096
|
|
18
|
+
// 4096. Mirrors /v1/decisions; lifecycle is
|
|
17
19
|
// open->resolved->closed (no supersede), so linkedMemoryIds replaces
|
|
18
20
|
// supersedesDecisionId on create.
|
|
19
21
|
export async function handleCreateIncident({ req, res, opts }) {
|
|
@@ -43,8 +45,8 @@ export async function handleCreateIncident({ req, res, opts }) {
|
|
|
43
45
|
if (!Array.isArray(linkedRaw)) {
|
|
44
46
|
throw new HttpError(400, 'linkedMemoryIds must be an array of memory ids');
|
|
45
47
|
}
|
|
46
|
-
if (linkedRaw.length >
|
|
47
|
-
throw new HttpError(400,
|
|
48
|
+
if (linkedRaw.length > MAX_LINKED_MEMORY_IDS) {
|
|
49
|
+
throw new HttpError(400, `linkedMemoryIds exceeds ${MAX_LINKED_MEMORY_IDS}-item cap`);
|
|
48
50
|
}
|
|
49
51
|
const isValidMemoryId = (item) => isJsonString(item) && item.length > 0 && item.length <= 4096;
|
|
50
52
|
if (!linkedRaw.every(isValidMemoryId)) {
|
|
@@ -4,7 +4,8 @@ import { buildGraphModel } from '../../graph-view.js';
|
|
|
4
4
|
import { MAX_ENTITY_NAME_LEN } from '../../graph/types.js';
|
|
5
5
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
6
6
|
import { assertCrossTenantAdmin, buildContextWithAuth, isLoopback } from '../auth.js';
|
|
7
|
-
import { getString, getStringArray, isJsonBoolean,
|
|
7
|
+
import { getString, getStringArray, isJsonBoolean, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
|
|
8
|
+
import { isJsonString } from '../../json.js';
|
|
8
9
|
const VALID_KINDS = new Set([
|
|
9
10
|
'raw',
|
|
10
11
|
'distilled',
|
|
@@ -38,7 +39,7 @@ export async function handleCreateMemory({ req, res, opts }) {
|
|
|
38
39
|
export async function handleGetGraph({ req, res, opts, query }) {
|
|
39
40
|
const entityRaw = query.get('entity');
|
|
40
41
|
// Cap at the graph entity-name cap (512), not the id-shaped 256, so a valid
|
|
41
|
-
// long decision/policy name remains focusable over HTTP
|
|
42
|
+
// long decision/policy name remains focusable over HTTP.
|
|
42
43
|
if (entityRaw !== null && entityRaw.length > MAX_ENTITY_NAME_LEN) {
|
|
43
44
|
throw new HttpError(400, `entity exceeds the ${MAX_ENTITY_NAME_LEN}-character cap`);
|
|
44
45
|
}
|
|
@@ -113,7 +114,7 @@ export async function handleApplyOutcome({ req, res, opts }) {
|
|
|
113
114
|
if (!idsRaw.every(isNonEmptyId)) {
|
|
114
115
|
throw new HttpError(400, 'ids must be an array of non-empty strings');
|
|
115
116
|
}
|
|
116
|
-
//
|
|
117
|
+
// DoS cap on ids.length. Each id triggers ~3 DB ops (readEntry +
|
|
117
118
|
// writeEntry + appendAuditEvent). N=1000 keeps per-request work bounded
|
|
118
119
|
// to sub-second wall time on SQLite hot path. Cap BEFORE buildContextWithAuth
|
|
119
120
|
// so attack traffic doesn't pay the api-key lookup cost.
|
|
@@ -141,8 +142,7 @@ export async function handleApplyOutcome({ req, res, opts }) {
|
|
|
141
142
|
// Tenant scope (Episode A follow-up tracked in TODOS.md): api.sleep operates
|
|
142
143
|
// on the WHOLE hippoRoot (cross-tenant by design, matching CLI cmdSleep).
|
|
143
144
|
// The loopback-only guard is the trust boundary today. Future non-loopback
|
|
144
|
-
// serving must also zero the cross-tenant counters for other tenants
|
|
145
|
-
// (D1 in docs/decisions/2026-05-24-blocked-items.md).
|
|
145
|
+
// serving must also zero the cross-tenant counters for other tenants.
|
|
146
146
|
export async function handleSleep({ req, res, opts }) {
|
|
147
147
|
// Defensive per-request loopback guard. Uses the canonical isLoopback()
|
|
148
148
|
// helper above so any future extension (additional mapped/IPv6 forms,
|
|
@@ -151,7 +151,7 @@ export async function handleSleep({ req, res, opts }) {
|
|
|
151
151
|
if (!isLoopback(req.socket.remoteAddress)) {
|
|
152
152
|
throw new HttpError(403, '/v1/sleep is loopback-only (host-wide consolidation; see CHANGELOG v1.11.4)');
|
|
153
153
|
}
|
|
154
|
-
//
|
|
154
|
+
// Admin-role gate. Forward-defensive: exists today
|
|
155
155
|
// under loopback-only enforcement (loopback fallback is admin by default;
|
|
156
156
|
// any Bearer-authed caller now carries an explicit role from the api_keys
|
|
157
157
|
// row). When non-loopback serving lands, this gate is the actual auth
|
|
@@ -168,7 +168,7 @@ export async function handleSleep({ req, res, opts }) {
|
|
|
168
168
|
if (noShareRaw !== undefined && !isJsonBoolean(noShareRaw)) {
|
|
169
169
|
throw new HttpError(400, 'no_share must be a boolean');
|
|
170
170
|
}
|
|
171
|
-
//
|
|
171
|
+
// sleepCtx already built above for the admin-role gate; reuse.
|
|
172
172
|
const result = await sleep(sleepCtx, {
|
|
173
173
|
dryRun: dryRunRaw === true,
|
|
174
174
|
noShare: noShareRaw === true,
|
|
@@ -3,7 +3,8 @@ import { closePolicy, loadPolicies, loadPoliciesAsOf, loadPolicyById, savePolicy
|
|
|
3
3
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
4
4
|
import { buildContextWithAuth } from '../auth.js';
|
|
5
5
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
7
|
+
import { isJsonString } from '../../json.js';
|
|
7
8
|
// HTTP-boundary check for an optional policy date field (validFrom/validTo).
|
|
8
9
|
// Type + length only; savePolicy/loadPoliciesAsOf normalize + format-validate the
|
|
9
10
|
// value (an unparseable date throws there -> mapped to 400). 64-char cap bounds a
|
|
@@ -19,7 +20,7 @@ function optionalDateField(raw, label) {
|
|
|
19
20
|
}
|
|
20
21
|
return raw;
|
|
21
22
|
}
|
|
22
|
-
// ── policies (
|
|
23
|
+
// ── policies (first-class object, bi-temporal-first) ──
|
|
23
24
|
//
|
|
24
25
|
// 6 routes: POST /v1/policies (new; processName-style body policyName +
|
|
25
26
|
// policyText + validFrom? + validTo?), GET /v1/policies (list, status filter),
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// /v1/predictions routes.
|
|
2
|
-
import { closePrediction, computePredictionBaserate, loadOpenPredictions, loadPredictionById, loadPredictionsByClass, savePrediction, VALID_CLOSURE_STATES } from '../../predictions/store.js';
|
|
3
|
-
import { HttpError, sendJson } from '../../http-util.js';
|
|
2
|
+
import { closePrediction, computePredictionBaserate, loadAllPredictions, loadOpenPredictions, loadPredictionById, loadPredictionsByClass, savePrediction, VALID_CLOSURE_STATES } from '../../predictions/store.js';
|
|
3
|
+
import { HttpError, MAX_ID_LEN, sendJson } from '../../http-util.js';
|
|
4
4
|
import { buildContextWithAuth } from '../auth.js';
|
|
5
5
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
6
|
-
import { isJsonNumber,
|
|
7
|
-
|
|
8
|
-
//
|
|
6
|
+
import { isJsonNumber, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
7
|
+
import { isJsonString } from '../../json.js';
|
|
8
|
+
// ── prediction first-class object ──
|
|
9
9
|
//
|
|
10
10
|
// 4 routes: POST /v1/predictions (create), GET /v1/predictions (list),
|
|
11
11
|
// GET /v1/predictions/:id (show), POST /v1/predictions/:id/close (close).
|
|
12
12
|
// All Bearer-authed + tenant-scoped via buildContextWithAuth. closure_state
|
|
13
13
|
// validated against VALID_CLOSURE_STATES (3 states). DoS caps on claim
|
|
14
|
-
// (4096 chars) + closureNote (2048 chars)
|
|
14
|
+
// (4096 chars) + closureNote (2048 chars).
|
|
15
15
|
export async function handleCreatePrediction({ req, res, opts }) {
|
|
16
16
|
const ctx = await buildContextWithAuth(req, opts);
|
|
17
17
|
const body = await parseJsonBody(req, ctx);
|
|
@@ -68,12 +68,9 @@ export async function handleListPredictions({ req, res, opts, query }) {
|
|
|
68
68
|
const ctx = await buildContextWithAuth(req, opts);
|
|
69
69
|
let predictions;
|
|
70
70
|
if (status === 'all') {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else {
|
|
75
|
-
predictions = loadOpenPredictions(opts.hippoRoot, ctx.tenantId, { limit: limit + 1, after });
|
|
76
|
-
}
|
|
71
|
+
predictions = classTag
|
|
72
|
+
? loadPredictionsByClass(opts.hippoRoot, ctx.tenantId, classTag, { limit: limit + 1, after })
|
|
73
|
+
: loadAllPredictions(opts.hippoRoot, ctx.tenantId, { limit: limit + 1, after });
|
|
77
74
|
}
|
|
78
75
|
else if (status === 'open') {
|
|
79
76
|
predictions = loadOpenPredictions(opts.hippoRoot, ctx.tenantId, {
|
|
@@ -99,7 +96,7 @@ export async function handleListPredictions({ req, res, opts, query }) {
|
|
|
99
96
|
sendJson(res, 200, { predictions: page.items, next_cursor: page.nextCursor });
|
|
100
97
|
return;
|
|
101
98
|
}
|
|
102
|
-
//
|
|
99
|
+
// Reference-class / planning-fallacy detector.
|
|
103
100
|
// Order matters: this must match BEFORE /v1/predictions/:id since 'stats'
|
|
104
101
|
// is not a number — the :id regex requires \d+ so they don't conflict,
|
|
105
102
|
// but routing this first avoids the dispatch order risk.
|
|
@@ -108,8 +105,8 @@ export async function handlePredictionStats({ req, res, opts, query }) {
|
|
|
108
105
|
if (!classTag || classTag.length === 0) {
|
|
109
106
|
throw new HttpError(400, 'class param is required');
|
|
110
107
|
}
|
|
111
|
-
if (classTag.length >
|
|
112
|
-
throw new HttpError(400,
|
|
108
|
+
if (classTag.length > MAX_ID_LEN) {
|
|
109
|
+
throw new HttpError(400, `class exceeds ${MAX_ID_LEN}-character cap`);
|
|
113
110
|
}
|
|
114
111
|
const ctx = await buildContextWithAuth(req, opts);
|
|
115
112
|
const baserate = computePredictionBaserate(opts.hippoRoot, ctx.tenantId, classTag, ctx.actor.subject);
|
|
@@ -3,7 +3,8 @@ import { closeProcess, loadProcessById, loadProcesses, saveProcess, VALID_PROCES
|
|
|
3
3
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
4
4
|
import { buildContextWithAuth } from '../auth.js';
|
|
5
5
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
7
|
+
import { isJsonString } from '../../json.js';
|
|
7
8
|
// HTTP-boundary validation for a process `steps` body (untrusted). Returns the
|
|
8
9
|
// step strings (saveProcess re-validates + trims, this is the fail-fast 400
|
|
9
10
|
// gate). Caps mirror src/processes.ts MAX_PROCESS_STEPS / MAX_PROCESS_STEP_LEN.
|
|
@@ -30,7 +31,7 @@ function validateProcessStepsBody(raw) {
|
|
|
30
31
|
// SAFETY: every item in raw was confirmed to be a string in the loop above.
|
|
31
32
|
return raw;
|
|
32
33
|
}
|
|
33
|
-
// ── processes (
|
|
34
|
+
// ── processes (first-class object) ──
|
|
34
35
|
//
|
|
35
36
|
// 5 routes: POST /v1/processes (new; body processName + steps[] + description),
|
|
36
37
|
// GET /v1/processes (list, status filter), GET /v1/processes/:id (show),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// /v1/project-briefs routes.
|
|
2
|
-
import { assembleBriefFromReceipts, closeProjectBrief, loadProjectBriefById, loadProjectBriefs, refreshBrief, saveProjectBrief, VALID_BRIEF_STATES } from '../../project-briefs.js';
|
|
2
|
+
import { assembleBriefFromReceipts, closeProjectBrief, loadProjectBriefById, loadProjectBriefs, MAX_REPO_LEN, refreshBrief, saveProjectBrief, VALID_BRIEF_STATES } from '../../project-briefs.js';
|
|
3
3
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
4
4
|
import { buildContextWithAuth } from '../auth.js';
|
|
5
5
|
import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
|
|
7
|
+
import { isJsonString } from '../../json.js';
|
|
8
|
+
// ── project_brief routes ──
|
|
8
9
|
//
|
|
9
10
|
// 6 routes: POST /v1/project-briefs (new; body repo + summary), GET
|
|
10
11
|
// /v1/project-briefs (list; status + repo filter; shared parseListLimit), POST
|
|
@@ -21,8 +22,8 @@ export async function handleCreateProjectBrief({ req, res, opts }) {
|
|
|
21
22
|
if (!isJsonString(repo) || repo.trim().length === 0) {
|
|
22
23
|
throw new HttpError(400, 'repo is required (non-empty string)');
|
|
23
24
|
}
|
|
24
|
-
if (repo.length >
|
|
25
|
-
throw new HttpError(400,
|
|
25
|
+
if (repo.length > MAX_REPO_LEN) {
|
|
26
|
+
throw new HttpError(400, `repo exceeds ${MAX_REPO_LEN}-character cap`);
|
|
26
27
|
}
|
|
27
28
|
const summary = body['summary'];
|
|
28
29
|
if (!isJsonString(summary) || summary.trim().length === 0) {
|
|
@@ -68,8 +69,8 @@ export async function handleRefreshProjectBrief({ req, res, opts }) {
|
|
|
68
69
|
if (!isJsonString(repo) || repo.trim().length === 0) {
|
|
69
70
|
throw new HttpError(400, 'repo is required (non-empty string)');
|
|
70
71
|
}
|
|
71
|
-
if (repo.length >
|
|
72
|
-
throw new HttpError(400,
|
|
72
|
+
if (repo.length > MAX_REPO_LEN) {
|
|
73
|
+
throw new HttpError(400, `repo exceeds ${MAX_REPO_LEN}-character cap`);
|
|
73
74
|
}
|
|
74
75
|
const dryRun = body['dryRun'] === true;
|
|
75
76
|
if (dryRun) {
|