@anthusai/papyrus 0.0.0 → 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "papyrus_console_chat_responder"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+
6
+ [dependencies]
7
+ anyhow = "1.0.102"
8
+ aws-config = "1.8.17"
9
+ aws-credential-types = "1.2.14"
10
+ aws-sdk-dynamodb = "1.112.0"
11
+ aws-sdk-ssm = "1.110.0"
12
+ aws-sigv4 = { version = "1.4.4", features = ["http1"] }
13
+ aws_lambda_events = "1.2.0"
14
+ base64 = "0.22.1"
15
+ chrono = { version = "0.4.44", features = ["serde"] }
16
+ futures-util = "0.3.31"
17
+ http = "1.4.0"
18
+ lambda_runtime = "1.2.0"
19
+ reqwest = { version = "0.13.3", features = ["json", "stream"] }
20
+ serde = { version = "1.0.228", features = ["derive"] }
21
+ serde_json = "1.0.150"
22
+ sha2 = "0.11.0"
23
+ tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread", "io-util", "process", "time"] }
24
+ tracing = "0.1.44"
25
+ tracing-subscriber = "0.3.23"
26
+ uuid = { version = "1.23.1", features = ["v4"] }
@@ -0,0 +1,19 @@
1
+ FROM rust:1.91.1-bookworm AS build
2
+ WORKDIR /workspace/amplify/functions/console-chat-responder
3
+
4
+ COPY amplify/functions/console-chat-responder/Cargo.toml amplify/functions/console-chat-responder/Cargo.lock ./
5
+ COPY amplify/functions/console-chat-responder/src ./src
6
+ RUN cargo build --release
7
+
8
+ FROM public.ecr.aws/lambda/provided:al2023
9
+ COPY --from=build /workspace/amplify/functions/console-chat-responder/target/release/papyrus_console_chat_responder /var/runtime/bootstrap
10
+ RUN dnf --setopt=cachedir=/tmp/dnf-cache install -y python3 python3-pip \
11
+ && python3 -m pip install --no-cache-dir PyYAML botocore tiktoken \
12
+ && python3 -m pip install --no-cache-dir tactus==0.44.1 \
13
+ && dnf clean all \
14
+ && rm -rf /tmp/dnf-cache /var/cache/dnf /var/cache/yum
15
+ COPY src /opt/papyrus/src
16
+ COPY amplify/functions/console-chat-responder/py/execute_tactus_runner.py /opt/papyrus/execute_tactus_runner.py
17
+ ENV PYTHONPATH=/opt/papyrus/src
18
+ RUN chmod +x /var/runtime/bootstrap
19
+ CMD ["/var/runtime/bootstrap"]
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ SEARCH_PATHS = [
11
+ Path("/opt/papyrus/src"),
12
+ Path.cwd() / "src",
13
+ ]
14
+ for root in SEARCH_PATHS:
15
+ if root.exists() and str(root) not in sys.path:
16
+ sys.path.insert(0, str(root))
17
+
18
+
19
+ def _ensure_graphql_authoring_jwt() -> None:
20
+ if not os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "").strip():
21
+ return
22
+ from papyrus_content.env import ensure_graphql_authoring_jwt # type: ignore
23
+
24
+ ensure_graphql_authoring_jwt()
25
+
26
+
27
+ def _error(code: str, message: str, *, retryable: bool = False) -> dict[str, Any]:
28
+ return {
29
+ "ok": False,
30
+ "error": {
31
+ "code": code,
32
+ "message": message,
33
+ "retryable": retryable,
34
+ },
35
+ }
36
+
37
+
38
+ def _docs_index() -> dict[str, Any]:
39
+ try:
40
+ from papyrus_newsroom.tactus_runtime import DOCS # type: ignore
41
+ except Exception as exc:
42
+ return _error("runner_unavailable", f"Unable to import papyrus_newsroom.tactus_runtime: {exc}")
43
+
44
+ entries: list[dict[str, str]] = []
45
+ for doc in DOCS.values():
46
+ entries.append(
47
+ {
48
+ "id": str(doc.get("id") or ""),
49
+ "title": str(doc.get("title") or ""),
50
+ "summary": str(doc.get("summary") or ""),
51
+ "namespace": str(doc.get("namespace") or ""),
52
+ }
53
+ )
54
+ entries.sort(key=lambda entry: entry["id"])
55
+ return {"entries": entries}
56
+
57
+
58
+ def _coerce_web_ui_context(payload: Any) -> dict[str, Any] | None:
59
+ if not isinstance(payload, dict):
60
+ return None
61
+ web_path = str(payload.get("webPath") or payload.get("web_path") or "").strip()
62
+ papyrus_location_uri = str(
63
+ payload.get("papyrusLocationUri") or payload.get("papyrus_location_uri") or ""
64
+ ).strip()
65
+ if not web_path and not papyrus_location_uri:
66
+ return None
67
+ context = dict(payload)
68
+ if web_path:
69
+ context["webPath"] = web_path
70
+ if papyrus_location_uri:
71
+ context["papyrusLocationUri"] = papyrus_location_uri
72
+ return context
73
+
74
+
75
+ def _execute_tactus(arguments: dict[str, Any], web_ui_context: dict[str, Any] | None = None) -> dict[str, Any]:
76
+ try:
77
+ from papyrus_newsroom.tactus_runtime import execute_tactus_harnessed # type: ignore
78
+ except Exception as exc:
79
+ return _error("runner_unavailable", f"Unable to import papyrus_newsroom.tactus_runtime: {exc}")
80
+
81
+ try:
82
+ return execute_tactus_harnessed(
83
+ str(arguments.get("tactus") or ""),
84
+ harness=str(arguments.get("harness") or "raw"),
85
+ assignment_id=str(arguments.get("assignment_id") or arguments.get("assignmentId") or ""),
86
+ assignment_item_json=str(arguments.get("assignment_item_json") or arguments.get("assignmentItemJson") or ""),
87
+ corpus_key=str(arguments.get("corpus_key") or arguments.get("corpusKey") or ""),
88
+ max_evidence_items=int(arguments.get("max_evidence_items") or arguments.get("maxEvidenceItems") or 20),
89
+ research_mode=str(arguments.get("research_mode") or arguments.get("researchMode") or ""),
90
+ web_ui_context=web_ui_context,
91
+ )
92
+ except Exception as exc:
93
+ return _error("tactus_execution_failed", f"Unexpected error: {exc}")
94
+
95
+
96
+ def main() -> None:
97
+ _ensure_graphql_authoring_jwt()
98
+ raw = sys.stdin.read()
99
+ payload = json.loads(raw or "{}")
100
+ mode = str(payload.get("mode") or "").strip()
101
+
102
+ if mode == "docs_index":
103
+ print(json.dumps(_docs_index()))
104
+ return
105
+
106
+ if mode == "execute_tactus":
107
+ args = payload.get("arguments") if isinstance(payload.get("arguments"), dict) else {}
108
+ web_ui_context = _coerce_web_ui_context(payload.get("webUi") or payload.get("web_ui"))
109
+ print(json.dumps(_execute_tactus(args, web_ui_context=web_ui_context)))
110
+ return
111
+
112
+ print(json.dumps(_error("invalid_mode", f"Unsupported mode: {mode}")))
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,153 @@
1
+ import { Duration, NestedStack, Stack } from "aws-cdk-lib";
2
+ import { Platform } from "aws-cdk-lib/aws-ecr-assets";
3
+ import { Repository } from "aws-cdk-lib/aws-ecr";
4
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
5
+ import { Architecture, CfnEventSourceMapping, DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda";
6
+ export class ConsoleChatResponderStack extends NestedStack {
7
+ responderFunction;
8
+ constructor(scope, id, props) {
9
+ super(scope, id, props);
10
+ const responseTarget = props.responseTarget?.trim() || "cloud";
11
+ const environment = {
12
+ PAPYRUS_MESSAGE_TABLE_NAME: props.messageTable.tableName,
13
+ PAPYRUS_MESSAGE_THREAD_TABLE_NAME: props.threadTable.tableName,
14
+ PAPYRUS_MESSAGE_THREAD_SEQUENCE_INDEX_NAME: "messagesByThreadSequence",
15
+ PAPYRUS_GRAPHQL_ENDPOINT: props.graphqlEndpoint,
16
+ PAPYRUS_CONSOLE_RESPONSE_TARGET: responseTarget,
17
+ PAPYRUS_CONSOLE_MODEL: props.model?.trim() || process.env.PAPYRUS_CONSOLE_MODEL || "gpt-5-nano",
18
+ PAPYRUS_CONSOLE_CONTEXT_CACHE_ROOT: "/tmp/papyrus-console/thread-context",
19
+ PAPYRUS_CONSOLE_STATIC_CONTEXT_TTL_SECONDS: process.env.PAPYRUS_CONSOLE_STATIC_CONTEXT_TTL_SECONDS || "900",
20
+ PAPYRUS_EXECUTE_TACTUS_RUNNER: process.env.PAPYRUS_EXECUTE_TACTUS_RUNNER || "/opt/papyrus/execute_tactus_runner.py",
21
+ PAPYRUS_EXECUTE_TACTUS_TIMEOUT_SECONDS: process.env.PAPYRUS_EXECUTE_TACTUS_TIMEOUT_SECONDS || "30",
22
+ PAPYRUS_JWT_ISSUER: process.env.PAPYRUS_JWT_ISSUER || "papyrus-cli",
23
+ PAPYRUS_JWT_SUBJECT: process.env.PAPYRUS_JWT_SUBJECT || "papyrus-cli",
24
+ PAPYRUS_JWT_AUDIENCE: process.env.PAPYRUS_JWT_AUDIENCE || "papyrus-authoring",
25
+ PAPYRUS_JWT_GROUPS: process.env.PAPYRUS_JWT_GROUPS || "editor",
26
+ ...(props.amplifySsmEnvConfig ? { AMPLIFY_SSM_ENV_CONFIG: props.amplifySsmEnvConfig } : {}),
27
+ };
28
+ const imageUri = props.prebuiltImageUri?.trim() || process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim() || "";
29
+ const allowLocalImageBuild = parseBooleanEnv(process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD);
30
+ let code;
31
+ if (imageUri) {
32
+ const { repositoryName, tagOrDigest } = parseEcrImageUri(imageUri);
33
+ assertArm64ImageUri(imageUri, tagOrDigest);
34
+ const repository = Repository.fromRepositoryName(this, "ConsoleChatResponderRepository", repositoryName);
35
+ code = DockerImageCode.fromEcr(repository, { tagOrDigest });
36
+ }
37
+ else {
38
+ if (!allowLocalImageBuild) {
39
+ throw new Error("ConsoleChatResponder requires PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI (prebuilt ECR image). "
40
+ + "Set PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true only when intentionally using local Docker image builds.");
41
+ }
42
+ code = DockerImageCode.fromImageAsset(props.projectRoot, {
43
+ file: "amplify/functions/console-chat-responder/Dockerfile",
44
+ platform: Platform.LINUX_ARM64,
45
+ exclude: [
46
+ ".git",
47
+ ".env",
48
+ ".env.*",
49
+ ".next",
50
+ ".amplify",
51
+ "node_modules",
52
+ "coverage",
53
+ "dist",
54
+ "out",
55
+ "corpora",
56
+ ".papyrus-runs",
57
+ "amplify_outputs.json",
58
+ "amplify_outputs.json.*",
59
+ "amplify/functions/console-chat-responder/target",
60
+ ],
61
+ });
62
+ }
63
+ this.responderFunction = new DockerImageFunction(this, "ConsoleChatResponderFunction", {
64
+ code,
65
+ architecture: Architecture.ARM_64,
66
+ timeout: Duration.minutes(2),
67
+ memorySize: 1024,
68
+ environment,
69
+ description: "Rust responder for Papyrus editor console chat Message stream",
70
+ });
71
+ new CfnEventSourceMapping(this, "ConsoleResponderMessageStreamMapping", {
72
+ batchSize: 1,
73
+ eventSourceArn: props.messageStreamArn,
74
+ functionName: this.responderFunction.functionArn,
75
+ functionResponseTypes: ["ReportBatchItemFailures"],
76
+ maximumRetryAttempts: 2,
77
+ startingPosition: "LATEST",
78
+ });
79
+ props.messageTable.grantReadWriteData(this.responderFunction);
80
+ props.threadTable.grantReadWriteData(this.responderFunction);
81
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
82
+ effect: Effect.ALLOW,
83
+ actions: ["dynamodb:Query"],
84
+ resources: [
85
+ `${props.messageTable.tableArn}/index/*`,
86
+ `${props.threadTable.tableArn}/index/*`,
87
+ ],
88
+ }));
89
+ const stack = Stack.of(this);
90
+ // execute_tactus_runner mints PAPYRUS_GRAPHQL_JWT via boto3 get_parameters; branch policies
91
+ // often grant GetParameters on path prefixes, not GetParameter per secret name.
92
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
93
+ effect: Effect.ALLOW,
94
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
95
+ resources: [
96
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/papyrus/*`,
97
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/papyrus/*`,
98
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/*`,
99
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/dbsyytcm9drqa/*`,
100
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/dbsyytcm9drqa/*`,
101
+ ],
102
+ }));
103
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
104
+ effect: Effect.ALLOW,
105
+ actions: ["appsync:GraphQL"],
106
+ resources: ["*"],
107
+ }));
108
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
109
+ effect: Effect.ALLOW,
110
+ actions: [
111
+ "dynamodb:DescribeStream",
112
+ "dynamodb:GetRecords",
113
+ "dynamodb:GetShardIterator",
114
+ "dynamodb:ListStreams",
115
+ ],
116
+ resources: [props.messageStreamArn],
117
+ }));
118
+ }
119
+ }
120
+ function parseBooleanEnv(value) {
121
+ if (!value)
122
+ return false;
123
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
124
+ }
125
+ function parseEcrImageUri(imageUri) {
126
+ const trimmed = imageUri.trim();
127
+ const hostAndPath = trimmed.split("/");
128
+ if (hostAndPath.length < 2) {
129
+ throw new Error(`Invalid PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI: ${trimmed}`);
130
+ }
131
+ const imagePath = hostAndPath.slice(1).join("/");
132
+ if (imagePath.includes("@sha256:")) {
133
+ const [repositoryName, digest] = imagePath.split("@", 2);
134
+ if (!repositoryName || !digest) {
135
+ throw new Error(`Invalid PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI digest form: ${trimmed}`);
136
+ }
137
+ return { repositoryName, tagOrDigest: digest };
138
+ }
139
+ const colonIndex = imagePath.lastIndexOf(":");
140
+ if (colonIndex <= 0 || colonIndex === imagePath.length - 1) {
141
+ throw new Error(`PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI must include a tag (repo:tag) or digest: ${trimmed}`);
142
+ }
143
+ return {
144
+ repositoryName: imagePath.slice(0, colonIndex),
145
+ tagOrDigest: imagePath.slice(colonIndex + 1),
146
+ };
147
+ }
148
+ function assertArm64ImageUri(imageUri, tagOrDigest) {
149
+ const normalized = `${imageUri} ${tagOrDigest}`.toLowerCase();
150
+ if (normalized.includes("amd64") || normalized.includes("x86_64")) {
151
+ throw new Error(`PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI must reference an ARM64 image, but received: ${imageUri}`);
152
+ }
153
+ }
@@ -0,0 +1,179 @@
1
+ import { Duration, NestedStack, Stack, type NestedStackProps } from "aws-cdk-lib";
2
+ import { Platform } from "aws-cdk-lib/aws-ecr-assets";
3
+ import { Repository } from "aws-cdk-lib/aws-ecr";
4
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
5
+ import { Architecture, CfnEventSourceMapping, DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda";
6
+ import type { ITable } from "aws-cdk-lib/aws-dynamodb";
7
+ import { Construct } from "constructs";
8
+
9
+ export type ConsoleChatResponderStackProps = NestedStackProps & {
10
+ messageTable: ITable;
11
+ messageStreamArn: string;
12
+ threadTable: ITable;
13
+ projectRoot: string;
14
+ graphqlEndpoint: string;
15
+ amplifySsmEnvConfig?: string;
16
+ responseTarget?: string;
17
+ model?: string;
18
+ prebuiltImageUri?: string;
19
+ };
20
+
21
+ export class ConsoleChatResponderStack extends NestedStack {
22
+ public readonly responderFunction: DockerImageFunction;
23
+
24
+ constructor(scope: Construct, id: string, props: ConsoleChatResponderStackProps) {
25
+ super(scope, id, props);
26
+
27
+ const responseTarget = props.responseTarget?.trim() || "cloud";
28
+ const environment: Record<string, string> = {
29
+ PAPYRUS_MESSAGE_TABLE_NAME: props.messageTable.tableName,
30
+ PAPYRUS_MESSAGE_THREAD_TABLE_NAME: props.threadTable.tableName,
31
+ PAPYRUS_MESSAGE_THREAD_SEQUENCE_INDEX_NAME: "messagesByThreadSequence",
32
+ PAPYRUS_GRAPHQL_ENDPOINT: props.graphqlEndpoint,
33
+ PAPYRUS_CONSOLE_RESPONSE_TARGET: responseTarget,
34
+ PAPYRUS_CONSOLE_MODEL: props.model?.trim() || process.env.PAPYRUS_CONSOLE_MODEL || "gpt-5-nano",
35
+ PAPYRUS_CONSOLE_CONTEXT_CACHE_ROOT: "/tmp/papyrus-console/thread-context",
36
+ PAPYRUS_CONSOLE_STATIC_CONTEXT_TTL_SECONDS: process.env.PAPYRUS_CONSOLE_STATIC_CONTEXT_TTL_SECONDS || "900",
37
+ PAPYRUS_EXECUTE_TACTUS_RUNNER: process.env.PAPYRUS_EXECUTE_TACTUS_RUNNER || "/opt/papyrus/execute_tactus_runner.py",
38
+ PAPYRUS_EXECUTE_TACTUS_TIMEOUT_SECONDS: process.env.PAPYRUS_EXECUTE_TACTUS_TIMEOUT_SECONDS || "30",
39
+ PAPYRUS_JWT_ISSUER: process.env.PAPYRUS_JWT_ISSUER || "papyrus-cli",
40
+ PAPYRUS_JWT_SUBJECT: process.env.PAPYRUS_JWT_SUBJECT || "papyrus-cli",
41
+ PAPYRUS_JWT_AUDIENCE: process.env.PAPYRUS_JWT_AUDIENCE || "papyrus-authoring",
42
+ PAPYRUS_JWT_GROUPS: process.env.PAPYRUS_JWT_GROUPS || "editor",
43
+ ...(props.amplifySsmEnvConfig ? { AMPLIFY_SSM_ENV_CONFIG: props.amplifySsmEnvConfig } : {}),
44
+ };
45
+
46
+ const imageUri = props.prebuiltImageUri?.trim() || process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim() || "";
47
+ const allowLocalImageBuild = parseBooleanEnv(process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD);
48
+ let code: DockerImageCode;
49
+ if (imageUri) {
50
+ const { repositoryName, tagOrDigest } = parseEcrImageUri(imageUri);
51
+ assertArm64ImageUri(imageUri, tagOrDigest);
52
+ const repository = Repository.fromRepositoryName(this, "ConsoleChatResponderRepository", repositoryName);
53
+ code = DockerImageCode.fromEcr(repository, { tagOrDigest });
54
+ } else {
55
+ if (!allowLocalImageBuild) {
56
+ throw new Error(
57
+ "ConsoleChatResponder requires PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI (prebuilt ECR image). "
58
+ + "Set PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true only when intentionally using local Docker image builds.",
59
+ );
60
+ }
61
+ code = DockerImageCode.fromImageAsset(props.projectRoot, {
62
+ file: "amplify/functions/console-chat-responder/Dockerfile",
63
+ platform: Platform.LINUX_ARM64,
64
+ exclude: [
65
+ ".git",
66
+ ".env",
67
+ ".env.*",
68
+ ".next",
69
+ ".amplify",
70
+ "node_modules",
71
+ "coverage",
72
+ "dist",
73
+ "out",
74
+ "corpora",
75
+ ".papyrus-runs",
76
+ "amplify_outputs.json",
77
+ "amplify_outputs.json.*",
78
+ "amplify/functions/console-chat-responder/target",
79
+ ],
80
+ });
81
+ }
82
+
83
+ this.responderFunction = new DockerImageFunction(this, "ConsoleChatResponderFunction", {
84
+ code,
85
+ architecture: Architecture.ARM_64,
86
+ timeout: Duration.minutes(2),
87
+ memorySize: 1024,
88
+ environment,
89
+ description: "Rust responder for Papyrus editor console chat Message stream",
90
+ });
91
+
92
+ new CfnEventSourceMapping(this, "ConsoleResponderMessageStreamMapping", {
93
+ batchSize: 1,
94
+ eventSourceArn: props.messageStreamArn,
95
+ functionName: this.responderFunction.functionArn,
96
+ functionResponseTypes: ["ReportBatchItemFailures"],
97
+ maximumRetryAttempts: 2,
98
+ startingPosition: "LATEST",
99
+ });
100
+
101
+ props.messageTable.grantReadWriteData(this.responderFunction);
102
+ props.threadTable.grantReadWriteData(this.responderFunction);
103
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
104
+ effect: Effect.ALLOW,
105
+ actions: ["dynamodb:Query"],
106
+ resources: [
107
+ `${props.messageTable.tableArn}/index/*`,
108
+ `${props.threadTable.tableArn}/index/*`,
109
+ ],
110
+ }));
111
+ const stack = Stack.of(this);
112
+ // execute_tactus_runner mints PAPYRUS_GRAPHQL_JWT via boto3 get_parameters; branch policies
113
+ // often grant GetParameters on path prefixes, not GetParameter per secret name.
114
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
115
+ effect: Effect.ALLOW,
116
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
117
+ resources: [
118
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/papyrus/*`,
119
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/papyrus/*`,
120
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/*`,
121
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/dbsyytcm9drqa/*`,
122
+ `arn:aws:ssm:${stack.region}:${stack.account}:parameter/amplify/shared/dbsyytcm9drqa/*`,
123
+ ],
124
+ }));
125
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
126
+ effect: Effect.ALLOW,
127
+ actions: ["appsync:GraphQL"],
128
+ resources: ["*"],
129
+ }));
130
+ this.responderFunction.addToRolePolicy(new PolicyStatement({
131
+ effect: Effect.ALLOW,
132
+ actions: [
133
+ "dynamodb:DescribeStream",
134
+ "dynamodb:GetRecords",
135
+ "dynamodb:GetShardIterator",
136
+ "dynamodb:ListStreams",
137
+ ],
138
+ resources: [props.messageStreamArn],
139
+ }));
140
+ }
141
+ }
142
+
143
+ function parseBooleanEnv(value: string | undefined): boolean {
144
+ if (!value) return false;
145
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
146
+ }
147
+
148
+ function parseEcrImageUri(imageUri: string): { repositoryName: string; tagOrDigest: string } {
149
+ const trimmed = imageUri.trim();
150
+ const hostAndPath = trimmed.split("/");
151
+ if (hostAndPath.length < 2) {
152
+ throw new Error(`Invalid PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI: ${trimmed}`);
153
+ }
154
+ const imagePath = hostAndPath.slice(1).join("/");
155
+ if (imagePath.includes("@sha256:")) {
156
+ const [repositoryName, digest] = imagePath.split("@", 2);
157
+ if (!repositoryName || !digest) {
158
+ throw new Error(`Invalid PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI digest form: ${trimmed}`);
159
+ }
160
+ return { repositoryName, tagOrDigest: digest };
161
+ }
162
+ const colonIndex = imagePath.lastIndexOf(":");
163
+ if (colonIndex <= 0 || colonIndex === imagePath.length - 1) {
164
+ throw new Error(`PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI must include a tag (repo:tag) or digest: ${trimmed}`);
165
+ }
166
+ return {
167
+ repositoryName: imagePath.slice(0, colonIndex),
168
+ tagOrDigest: imagePath.slice(colonIndex + 1),
169
+ };
170
+ }
171
+
172
+ function assertArm64ImageUri(imageUri: string, tagOrDigest: string): void {
173
+ const normalized = `${imageUri} ${tagOrDigest}`.toLowerCase();
174
+ if (normalized.includes("amd64") || normalized.includes("x86_64")) {
175
+ throw new Error(
176
+ `PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI must reference an ARM64 image, but received: ${imageUri}`,
177
+ );
178
+ }
179
+ }