@r8s/paperclip 0.2.0 → 0.2.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.
@@ -0,0 +1,155 @@
1
+ import { type DatabaseProps } from '@r8s/recipes';
2
+ export interface PaperclipProps {
3
+ /** Resource name / Instance CR name (defaults to 'paperclip') */
4
+ name?: string;
5
+ /** Kubernetes namespace (inherited from <Platform> unless set) */
6
+ namespace?: string;
7
+ /**
8
+ * App image tag (defaults to 'sso-oidc' — the Berget fork branch build
9
+ * for SSO/OIDC work; a named moving tag, Always-pulled on purpose).
10
+ */
11
+ version?: string;
12
+ /** Image repository (defaults to the Berget fork) */
13
+ repository?: string;
14
+ /** Public hostname for the web app and API (required) */
15
+ host: string;
16
+ /**
17
+ * Manual image pull secrets for the private ghcr image (defaults to
18
+ * ['ghcr-pull-secret'] — a fine-grained PAT read:packages Secret that
19
+ * is deliberately managed OUTSIDE Flux: rotate it by delete+recreate)
20
+ */
21
+ pullSecrets?: string[];
22
+ /**
23
+ * Operator chart version for the paperclip-operator (defaults to
24
+ * '0.19.0'). The operator owns the StatefulSet/PVC/ingress/probes —
25
+ * the Instance CR is the entire app definition.
26
+ */
27
+ operatorVersion?: string;
28
+ /**
29
+ * CNPG cluster name holding paperclip's data (defaults to 'paperclip-db').
30
+ * Credentials are CNPG-managed: the Instance references the generated
31
+ * `<db>-app` secret's `fqdn-uri` key (requires CNPG ≥ 1.20).
32
+ */
33
+ dbName?: string;
34
+ /** Number of CNPG instances (defaults to 2) */
35
+ dbInstances?: number;
36
+ /** CNPG data volume size (defaults to '20Gi') */
37
+ dbStorage?: string;
38
+ /** CNPG storage class (defaults to cluster default) */
39
+ dbStorageClass?: string;
40
+ /** CNPG backup configuration passed through to the Database recipe (continuous WAL + scheduled base backups to Scaleway in facit) */
41
+ backup?: DatabaseProps['backup'];
42
+ /**
43
+ * App-native database backups (sql dumps on the persistence volume).
44
+ * Defaults to facit: enabled, hourly, 7 days retention.
45
+ */
46
+ appBackup?: {
47
+ enabled?: boolean;
48
+ intervalMinutes?: number;
49
+ retentionDays?: number;
50
+ } | false;
51
+ /**
52
+ * Operator-managed persistence volume at /paperclip (storage dir
53
+ * /paperclip/storage, native backups /paperclip/backups).
54
+ * Defaults to 10Gi; storageClass optional.
55
+ */
56
+ storage?: {
57
+ size?: string;
58
+ storageClass?: string;
59
+ } | false;
60
+ /** Heartbeat scheduler (defaults to facit: every 30s) */
61
+ heartbeat?: {
62
+ enabled?: boolean;
63
+ intervalMS?: number;
64
+ } | false;
65
+ /**
66
+ * LLM access via the Berget gateway. `baseUrl` defaults to
67
+ * https://api.berget.ai/v1. The API key is provisioned through the
68
+ * Platform secrets backend (path `<path>/<name>/berget-ai`, key
69
+ * `api-key`) unless `apiKeySecretName` references a pre-created
70
+ * Secret (key: api-key). Rotation restarts the StatefulSet.
71
+ */
72
+ llm?: {
73
+ baseUrl?: string;
74
+ path?: string;
75
+ refreshAfter?: string;
76
+ };
77
+ /** Reference a pre-created LLM API key Secret (key: api-key) instead of backend provisioning */
78
+ apiKeySecretName?: string;
79
+ /**
80
+ * Better Auth secret — session signing. Provisioned through the
81
+ * Platform secrets backend (path `<path>/<name>/app`, key
82
+ * `better-auth-secret`) unless `secretsName` references a pre-created
83
+ * Secret (key: better-auth-secret). Rotation restarts the StatefulSet.
84
+ */
85
+ auth?: {
86
+ path?: string;
87
+ refreshAfter?: string;
88
+ disableSignUp?: boolean;
89
+ };
90
+ /** Reference a pre-created app secrets bundle (key: better-auth-secret) instead of backend provisioning */
91
+ secretsName?: string;
92
+ /**
93
+ * Berget model catalog env vars (OPENCODE_CONFIG_CONTENT +
94
+ * PAPERCLIP_ADAPTER_MODELS) wiring Paperclip to the Berget gateway.
95
+ * Defaults to the facit catalog (Kimi K3/K2.6, GLM-5.2/4.7, GPT-OSS
96
+ * 120B, Mistral Medium/Small, Qwen3.8 27B, Gemma 4 31B). Pass `false`
97
+ * to omit both vars.
98
+ */
99
+ modelCatalog?: {
100
+ opencodeConfig?: string;
101
+ adapterModels?: string;
102
+ } | false;
103
+ /** Extra ingress annotations merged over the facit defaults (body-size 50m, 300s timeouts, ssl-redirect) */
104
+ ingressAnnotations?: Record<string, string>;
105
+ /** Additional hostnames accepted by the deployment (defaults to [host]) */
106
+ allowedHostnames?: string[];
107
+ /** Requested resources for the app StatefulSet (defaults to facit: 512Mi/250m → 12Gi/2) */
108
+ resources?: {
109
+ requests?: {
110
+ cpu?: string;
111
+ memory?: string;
112
+ };
113
+ limits?: {
114
+ cpu?: string;
115
+ memory?: string;
116
+ };
117
+ };
118
+ /** TLS secret for ingress (defaults to `${name}-tls` via letsencrypt-prod) */
119
+ tls?: {
120
+ secretName: string;
121
+ clusterIssuer: string;
122
+ };
123
+ }
124
+ /**
125
+ * Paperclip — Berget's agent platform (tasks, documents, agent orchestration),
126
+ * facit-aligned on the paperclip-operator.
127
+ *
128
+ * @title Paperclip
129
+ * @category Agent Platforms
130
+ *
131
+ * The operator owns the workload: app instance → StatefulSet with PVC,
132
+ * ingress, probes, security context, heartbeat and app-native backups.
133
+ * This package declares the operator and owns everything AROUND it:
134
+ * - the external CNPG cluster (`credentialsMode: 'cnpg'` — the Instance
135
+ * references the operator-generated `<db>-app` `fqdn-uri` like facit)
136
+ * - the app secrets (better-auth) + LLM gateway key via the Platform
137
+ * secrets backend with rotation restart of the StatefulSet
138
+ * - the `paperclip.inc/v1alpha1` `Instance` CR with facit spec
139
+ *
140
+ * The image is private: `pullSecrets` defaults to `['ghcr-pull-secret']`
141
+ * — a fine-grained PAT (read:packages) Secret managed manually outside
142
+ * Flux (rotate by delete+recreate).
143
+ *
144
+ * @example
145
+ * import { Platform } from '@r8s/recipes'
146
+ * import { Paperclip } from '@r8s/paperclip'
147
+ *
148
+ * export default (
149
+ * <Platform secrets={{ backend: 'openbao', mount: 'secret', path: 'paperclip' }}>
150
+ * <Paperclip host="paperclip.example.com" />
151
+ * </Platform>
152
+ * )
153
+ */
154
+ export declare function Paperclip(props: PaperclipProps): import("@r8s/core").r8sElement;
155
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,cAAc,CAAA;AAGrB,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qIAAqI;IACrI,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAA;IAC3F;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAA;IAC1D,yDAAyD;IACzD,SAAS,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAA;IAC9D;;;;;;OAMG;IACH,GAAG,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChE,gGAAgG;IAChG,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;OAKG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACxE,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAA;IAC1E,4GAA4G;IAC5G,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3C,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,2FAA2F;IAC3F,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;QAC5C,MAAM,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAC3C,CAAA;IACD,8EAA8E;IAC9E,GAAG,CAAC,EAAE;QACJ,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,kCAiQ9C"}
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Paperclip = Paperclip;
4
+ const core_1 = require("@r8s/core");
5
+ const defaults_1 = require("@r8s/core/defaults");
6
+ const recipes_1 = require("@r8s/recipes");
7
+ const DEFAULT_RESOURCES = {
8
+ requests: { memory: '512Mi', cpu: '250m' },
9
+ limits: { memory: '12Gi', cpu: '2' },
10
+ };
11
+ const DEFAULT_OPENCODE_CONFIG = '{"provider":{"berget":{"npm":"@ai-sdk/openai-compatible","name":"Berget","options":{"baseURL":"https://api.berget.ai/v1","apiKey":"{env:OPENAI_API_KEY}"},"models":{"moonshotai/Kimi-K3":{"name":"Kimi K3"},"moonshotai/Kimi-K2.6":{"name":"Kimi K2.6"},"zai-org/GLM-5.2":{"name":"GLM-5.2"},"zai-org/GLM-4.7-FP8":{"name":"GLM-4.7"},"openai/gpt-oss-120b":{"name":"GPT-OSS 120B"},"mistralai/Mistral-Medium-3.5-128B":{"name":"Mistral Medium 3.5"},"mistralai/Mistral-Small-3.2-24B-Instruct-2506":{"name":"Mistral Small"},"Qwen/Qwen3.8-27B-FP8":{"name":"Qwen3.8 27B"},"google/gemma-4-31B-it":{"name":"Gemma 4 31B"}}}}}';
12
+ const DEFAULT_ADAPTER_MODELS = '{"opencode_local":[{"id":"berget/moonshotai/Kimi-K3","label":"Kimi K3"},{"id":"berget/moonshotai/Kimi-K2.6","label":"Kimi K2.6"},{"id":"berget/zai-org/GLM-5.2","label":"GLM-5.2"},{"id":"berget/zai-org/GLM-4.7-FP8","label":"GLM-4.7"},{"id":"berget/openai/gpt-oss-120b","label":"GPT-OSS 120B"},{"id":"berget/mistralai/Mistral-Medium-3.5-128B","label":"Mistral Medium 3.5"},{"id":"berget/mistralai/Mistral-Small-3.2-24B-Instruct-2506","label":"Mistral Small"},{"id":"berget/Qwen/Qwen3.8-27B-FP8","label":"Qwen3.8 27B"},{"id":"berget/google/gemma-4-31B-it","label":"Gemma 4 31B"}]}';
13
+ /**
14
+ * Paperclip — Berget's agent platform (tasks, documents, agent orchestration),
15
+ * facit-aligned on the paperclip-operator.
16
+ *
17
+ * @title Paperclip
18
+ * @category Agent Platforms
19
+ *
20
+ * The operator owns the workload: app instance → StatefulSet with PVC,
21
+ * ingress, probes, security context, heartbeat and app-native backups.
22
+ * This package declares the operator and owns everything AROUND it:
23
+ * - the external CNPG cluster (`credentialsMode: 'cnpg'` — the Instance
24
+ * references the operator-generated `<db>-app` `fqdn-uri` like facit)
25
+ * - the app secrets (better-auth) + LLM gateway key via the Platform
26
+ * secrets backend with rotation restart of the StatefulSet
27
+ * - the `paperclip.inc/v1alpha1` `Instance` CR with facit spec
28
+ *
29
+ * The image is private: `pullSecrets` defaults to `['ghcr-pull-secret']`
30
+ * — a fine-grained PAT (read:packages) Secret managed manually outside
31
+ * Flux (rotate by delete+recreate).
32
+ *
33
+ * @example
34
+ * import { Platform } from '@r8s/recipes'
35
+ * import { Paperclip } from '@r8s/paperclip'
36
+ *
37
+ * export default (
38
+ * <Platform secrets={{ backend: 'openbao', mount: 'secret', path: 'paperclip' }}>
39
+ * <Paperclip host="paperclip.example.com" />
40
+ * </Platform>
41
+ * )
42
+ */
43
+ function Paperclip(props) {
44
+ const { name = 'paperclip', namespace: namespaceProp, version = 'sso-oidc', repository = 'ghcr.io/berget-ai/paperclip', host, pullSecrets = ['ghcr-pull-secret'], operatorVersion = '0.19.0', dbName = 'paperclip-db', dbInstances = 2, dbStorage = '20Gi', dbStorageClass, backup, appBackup, storage, heartbeat, llm, apiKeySecretName, auth, secretsName, modelCatalog, ingressAnnotations = {}, allowedHostnames, resources = DEFAULT_RESOURCES, tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' }, } = props;
45
+ const namespace = (0, defaults_1.useNamespace)(namespaceProp);
46
+ const secretProvider = (0, core_1.useContext)(defaults_1.SecretContext);
47
+ const sharedOperators = (0, recipes_1.useOperators)();
48
+ const resources_ = [];
49
+ // --- Operator declaration (stateful workload owner) -----------------------
50
+ if (!sharedOperators.some((op) => op.name === 'paperclip-operator')) {
51
+ const paperclipOperator = {
52
+ name: 'paperclip-operator',
53
+ version: operatorVersion,
54
+ description: 'Paperclip operator — Instance CR → StatefulSet + PVC + ingress',
55
+ source: {
56
+ type: 'helm',
57
+ chart: 'paperclip-operator',
58
+ repository: 'oci://ghcr.io/paperclipinc/charts',
59
+ version: operatorVersion,
60
+ values: {
61
+ operator: {
62
+ resources: {
63
+ limits: { cpu: '500m', memory: '512Mi' },
64
+ requests: { cpu: '100m', memory: '128Mi' },
65
+ },
66
+ },
67
+ metrics: { enabled: true, serviceMonitor: { enabled: false } },
68
+ leaderElection: { enabled: false },
69
+ },
70
+ },
71
+ crds: ['instances.paperclip.inc'],
72
+ };
73
+ resources_.push((0, core_1.declareOperator)(paperclipOperator));
74
+ }
75
+ // --- App secrets (better-auth) + LLM key ----------------------------------
76
+ const appSecretsName = secretsName ?? `${name}-secrets`;
77
+ const apiKeyName = apiKeySecretName ?? 'berget-api-key';
78
+ if (!secretsName) {
79
+ if (!(0, recipes_1.canProvisionSecrets)(secretProvider)) {
80
+ throw (0, recipes_1.secretsRequiredError)('Paperclip', name, 'a Better Auth secret — it signs every user session', {
81
+ propName: 'secretsName',
82
+ exampleValue: `${name}-secrets`,
83
+ keys: ['better-auth-secret'],
84
+ });
85
+ }
86
+ resources_.push((0, core_1.jsx)(recipes_1.StaticSecret, {
87
+ name: `${name}-secrets`,
88
+ namespace,
89
+ path: auth?.path ?? `${secretProvider.path ?? name}/${name}/app`,
90
+ secretName: appSecretsName,
91
+ refreshAfter: auth?.refreshAfter,
92
+ keys: ['better-auth-secret'],
93
+ // The operator-managed workload is a StatefulSet, not a Deployment
94
+ restart: [{ kind: 'StatefulSet', name }],
95
+ }));
96
+ }
97
+ if (!apiKeySecretName) {
98
+ if (!(0, recipes_1.canProvisionSecrets)(secretProvider)) {
99
+ throw (0, recipes_1.secretsRequiredError)('Paperclip', name, 'an LLM API key for the Berget gateway (agents call models on behalf of users)', {
100
+ propName: 'apiKeySecretName',
101
+ exampleValue: 'berget-api-key',
102
+ keys: ['api-key'],
103
+ });
104
+ }
105
+ resources_.push((0, core_1.jsx)(recipes_1.StaticSecret, {
106
+ name: `berget-api-key`,
107
+ namespace,
108
+ path: llm?.path ?? `${secretProvider.path ?? name}/${name}/berget-ai`,
109
+ secretName: apiKeyName,
110
+ refreshAfter: llm?.refreshAfter ?? '3600s',
111
+ keys: ['api-key'],
112
+ restart: [{ kind: 'StatefulSet', name }],
113
+ }));
114
+ }
115
+ // --- External database (CNPG-managed credentials: `<db>-app` fqdn-uri) ----
116
+ resources_.push((0, core_1.jsx)(recipes_1.Database, {
117
+ name: dbName,
118
+ namespace,
119
+ instances: dbInstances,
120
+ storage: dbStorage,
121
+ ...(dbStorageClass ? { storageClass: dbStorageClass } : {}),
122
+ parameters: { max_connections: '200', effective_cache_size: '768MB' },
123
+ credentialsMode: 'cnpg',
124
+ ...(backup ? { backup } : {}),
125
+ }));
126
+ // --- App env (facit contract) ----------------------------------------------
127
+ const llmBaseUrl = llm?.baseUrl ?? 'https://api.berget.ai/v1';
128
+ const appNative = appBackup === false ? undefined : (appBackup ?? {});
129
+ const hb = heartbeat === false ? undefined : (heartbeat ?? {});
130
+ const store = storage === false ? undefined : (storage ?? {});
131
+ const catalog = modelCatalog === false ? undefined : (modelCatalog ?? {});
132
+ const env = [
133
+ { name: 'PAPERCLIP_TELEMETRY_DISABLED', value: '1' },
134
+ { name: 'OPENAI_BASE_URL', value: llmBaseUrl },
135
+ {
136
+ name: 'OPENAI_API_KEY',
137
+ valueFrom: { secretKeyRef: { name: apiKeyName, key: 'api-key' } },
138
+ },
139
+ { name: 'PAPERCLIP_SECRETS_PROVIDER', value: 'local_encrypted' },
140
+ { name: 'PAPERCLIP_STORAGE_PROVIDER', value: 'local_disk' },
141
+ { name: 'PAPERCLIP_STORAGE_LOCAL_DIR', value: '/paperclip/storage' },
142
+ ...(hb?.enabled !== false ? [{ name: 'HEARTBEAT_SCHEDULER_ENABLED', value: 'true' }] : []),
143
+ ...(appNative?.enabled !== false
144
+ ? [
145
+ { name: 'PAPERCLIP_DB_BACKUP_ENABLED', value: 'true' },
146
+ { name: 'PAPERCLIP_DB_BACKUP_DIR', value: '/paperclip/backups' },
147
+ ]
148
+ : []),
149
+ { name: 'PAPERCLIP_AUTH_BASE_URL_MODE', value: 'explicit' },
150
+ { name: 'PAPERCLIP_AUTH_PUBLIC_BASE_URL', value: `https://${host}` },
151
+ ...(catalog
152
+ ? [
153
+ {
154
+ name: 'OPENCODE_CONFIG_CONTENT',
155
+ value: catalog.opencodeConfig ?? DEFAULT_OPENCODE_CONFIG,
156
+ },
157
+ {
158
+ name: 'PAPERCLIP_ADAPTER_MODELS',
159
+ value: catalog.adapterModels ?? DEFAULT_ADAPTER_MODELS,
160
+ },
161
+ ]
162
+ : []),
163
+ ];
164
+ // --- Instance CR — the entire app definition (operator-owned) --------------
165
+ resources_.push((0, core_1.jsx)('Instance', {
166
+ apiVersion: 'paperclip.inc/v1alpha1',
167
+ kind: 'Instance',
168
+ metadata: { name, namespace, labels: { app: name } },
169
+ spec: {
170
+ image: {
171
+ repository,
172
+ tag: version,
173
+ pullPolicy: 'Always',
174
+ pullSecrets: pullSecrets.map((n) => ({ name: n })),
175
+ },
176
+ deployment: {
177
+ mode: 'authenticated',
178
+ exposure: 'public',
179
+ publicURL: `https://${host}`,
180
+ allowedHostnames: allowedHostnames ?? [host],
181
+ },
182
+ auth: {
183
+ disableSignUp: auth?.disableSignUp !== false,
184
+ secretRef: { name: appSecretsName, key: 'better-auth-secret' },
185
+ },
186
+ database: {
187
+ mode: 'external',
188
+ externalURLSecretRef: { name: `${dbName}-app`, key: 'fqdn-uri' },
189
+ },
190
+ adapters: { apiKeysSecretRef: { name: apiKeyName } },
191
+ storage: {
192
+ persistence: {
193
+ enabled: storage !== false,
194
+ size: store?.size ?? '10Gi',
195
+ ...(store?.storageClass ? { storageClass: store.storageClass } : {}),
196
+ },
197
+ },
198
+ resources,
199
+ networking: {
200
+ service: { type: 'ClusterIP', port: 3100 },
201
+ ingress: {
202
+ enabled: true,
203
+ ingressClassName: 'nginx',
204
+ hosts: [host],
205
+ tls: [{ hosts: [host], secretName: tls.secretName }],
206
+ annotations: {
207
+ 'cert-manager.io/cluster-issuer': tls.clusterIssuer,
208
+ 'external-dns.alpha.kubernetes.io/hostname': host,
209
+ 'nginx.ingress.kubernetes.io/ssl-redirect': 'true',
210
+ 'nginx.ingress.kubernetes.io/proxy-body-size': '50m',
211
+ 'nginx.ingress.kubernetes.io/proxy-read-timeout': '300',
212
+ 'nginx.ingress.kubernetes.io/proxy-send-timeout': '300',
213
+ 'nginx.ingress.kubernetes.io/proxy-http-version': '1.1',
214
+ ...ingressAnnotations,
215
+ },
216
+ },
217
+ },
218
+ probes: { type: 'auto' },
219
+ heartbeat: hb && hb.enabled !== false ? { enabled: true, intervalMS: hb.intervalMS ?? 30000 } : null,
220
+ backup: appNative && appNative.enabled !== false
221
+ ? {
222
+ appNative: {
223
+ enabled: true,
224
+ intervalMinutes: appNative.intervalMinutes ?? 60,
225
+ retentionDays: appNative.retentionDays ?? 7,
226
+ },
227
+ }
228
+ : null,
229
+ security: {
230
+ networkPolicy: { enabled: false },
231
+ seLinuxRelabel: false,
232
+ podSecurityContext: { fsGroup: 1000 },
233
+ containerSecurityContext: {
234
+ // The image runs as root — runAsNonRoot would crash-loop it
235
+ allowPrivilegeEscalation: false,
236
+ capabilities: { drop: ['ALL'] },
237
+ runAsNonRoot: false,
238
+ runAsUser: 0,
239
+ },
240
+ },
241
+ env,
242
+ },
243
+ }));
244
+ return (0, core_1.jsx)(core_1.Fragment, { children: resources_ });
245
+ }
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r8s/paperclip",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Paperclip — Berget's agent platform (tasks, documents, agent orchestration)",
5
5
  "license": "MIT",
6
6
  "author": "Berget AI AB",
@@ -37,5 +37,8 @@
37
37
  "devDependencies": {
38
38
  "typescript": "^5.3.0",
39
39
  "vitest": "^1.0.0"
40
- }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ]
41
44
  }
@@ -1,450 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, jsx } from '@r8s/core'
3
- import { Namespace, OperatorContext, SecretContext, RoutingContext } from '@r8s/core/defaults'
4
- import { runGuardrails, noPlaintextSecrets, validateResource } from '@r8s/core'
5
- import { operators } from '@r8s/crds'
6
- import type { r8sElement } from '@r8s/core'
7
-
8
- // Paperclip recipe tests:
9
- // 1. Operator declarations (cnpg via Database, deduped via OperatorContext)
10
- // 2. Rendering: defaults, sandbox agents (hardened sandbox, TCP probes),
11
- // all props, gateway/ingress adaptation
12
- // 3. Namespace inheritance from the Platform context
13
- // 4. Security: no plaintext credentials in rendered output
14
- import { Paperclip } from '../src/index'
15
-
16
- /** Render Paperclip inside a Platform-like secrets backend (OpenBao). */
17
- function renderPaperclip(props: Record<string, unknown>): ReturnType<typeof render> {
18
- return render(
19
- jsx(SecretContext.Provider, {
20
- value: { backend: 'openbao', mount: 'kv', path: 'test' },
21
- children: jsx(Paperclip, props as never),
22
- })
23
- )
24
- }
25
-
26
- /** Render Paperclip inside a Platform-like Namespace context (no explicit namespace prop). */
27
- function renderPaperclipInNamespace(
28
- namespaceValue: string,
29
- props: Record<string, unknown>
30
- ): ReturnType<typeof render> {
31
- return render(
32
- jsx(Namespace.Provider, {
33
- value: namespaceValue,
34
- children: jsx(SecretContext.Provider, {
35
- value: { backend: 'openbao', mount: 'kv', path: 'test' },
36
- children: jsx(Paperclip, props as never),
37
- }),
38
- })
39
- )
40
- }
41
-
42
- /** Render Paperclip wrapped only in an OperatorContext (no secrets backend). */
43
- function renderPaperclipWithContext(operators_: any[], props: Record<string, unknown>): r8sElement {
44
- return jsx(OperatorContext.Provider, {
45
- value: operators_,
46
- children: jsx(Paperclip, props as never),
47
- })
48
- }
49
-
50
- const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
51
-
52
- describe('operator declarations', () => {
53
- it('declares the cnpg operator via the Database recipe', () => {
54
- const result = renderPaperclip({ host: 'paperclip.example.com' })
55
- expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
56
- })
57
-
58
- it('declares cnpg exactly once even with sandbox agents (no redis for paperclip)', () => {
59
- const result = renderPaperclip({
60
- host: 'paperclip.example.com',
61
- agents: { sandboxReplicas: 3 },
62
- })
63
- const names = result.operators.map((op) => op.name)
64
- expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
65
- const kinds = result.resources.map((r) => r.kind)
66
- expect(kinds).not.toContain('RedisCluster')
67
- })
68
-
69
- it('deduplicates operators provided via context', () => {
70
- const result = render(
71
- renderPaperclipWithContext([operators['cnpg']()], {
72
- host: 'paperclip.example.com',
73
- secretsName: 'existing-secrets',
74
- })
75
- )
76
- const names = result.operators.map((op) => op.name)
77
- expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
78
- })
79
-
80
- it('allows version overrides through context operators', () => {
81
- const result = render(
82
- renderPaperclipWithContext([operators['cnpg']('1.25.0')], {
83
- host: 'paperclip.example.com',
84
- secretsName: 'existing-secrets',
85
- })
86
- )
87
- const cnpg = result.operators.filter((op) => op.name === 'cnpg')
88
- expect(cnpg).toHaveLength(1)
89
- expect(cnpg[0].version).toBe('1.25.0')
90
- })
91
- })
92
-
93
- describe('rendering defaults', () => {
94
- it('renders main deployment, service, database and endpoint', () => {
95
- const result = renderPaperclip({ host: 'paperclip.example.com' })
96
- const kinds = result.resources.map((r) => r.kind)
97
- expect(kinds).toContain('Deployment')
98
- expect(kinds).toContain('Service')
99
- expect(kinds).toContain('Ingress')
100
- expect(kinds).toContain('Cluster')
101
- })
102
-
103
- it('renders the main app with the paperclip image on port 3000', () => {
104
- const result = renderPaperclip({ host: 'paperclip.example.com' })
105
- const main = result.resources.find(
106
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip'
107
- ) as any
108
- expect(main).toBeDefined()
109
- const container = main.spec.template.spec.containers[0]
110
- expect(container.image).toBe('ghcr.io/berget-ai/paperclip:latest')
111
- expect(container.ports[0].containerPort).toBe(3000)
112
- expect(main.spec.replicas).toBe(2)
113
- })
114
-
115
- it('probes both workloads with TCP sockets (unknown HTTP health contract)', () => {
116
- const result = renderPaperclip({
117
- host: 'paperclip.example.com',
118
- agents: { sandboxReplicas: 2 },
119
- })
120
- for (const name of ['paperclip', 'paperclip-agent-sandbox']) {
121
- const deployment = result.resources.find(
122
- (r: any) => r.kind === 'Deployment' && r.metadata.name === name
123
- ) as any
124
- const container = deployment.spec.template.spec.containers[0]
125
- expect(container.livenessProbe.tcpSocket).toEqual({ port: 3000 })
126
- expect(container.readinessProbe.tcpSocket).toEqual({ port: 3000 })
127
- expect(container.livenessProbe.httpGet).toBeUndefined()
128
- expect(container.readinessProbe.httpGet).toBeUndefined()
129
- }
130
- })
131
-
132
- it('renders no sandbox deployment when agents is not set', () => {
133
- const result = renderPaperclip({ host: 'paperclip.example.com' })
134
- const names = result.resources
135
- .filter((r) => r.kind === 'Deployment')
136
- .map((d: any) => d.metadata.name)
137
- expect(names).toEqual(['paperclip'])
138
- })
139
-
140
- it('renders the sandbox agent deployment when agents is set', () => {
141
- const result = renderPaperclip({
142
- host: 'paperclip.example.com',
143
- agents: { sandboxReplicas: 3 },
144
- })
145
- const sandbox = result.resources.find(
146
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
147
- ) as any
148
- expect(sandbox).toBeDefined()
149
- expect(sandbox.spec.replicas).toBe(3)
150
- expect(sandbox.spec.template.spec.containers[0].image).toBe(
151
- 'ghcr.io/berget-ai/paperclip:latest'
152
- )
153
- expect(sandbox.spec.template.spec.containers[0].command).toEqual([
154
- 'paperclip',
155
- 'agent',
156
- '--sandbox',
157
- ])
158
- })
159
-
160
- it('hardens the sandbox with a locked-down securityContext', () => {
161
- const result = renderPaperclip({
162
- host: 'paperclip.example.com',
163
- agents: {},
164
- })
165
- const sandbox = result.resources.find(
166
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
167
- ) as any
168
- const podSpec = sandbox.spec.template.spec
169
- expect(podSpec.securityContext).toEqual({ seccompProfile: { type: 'RuntimeDefault' } })
170
- const securityContext = podSpec.containers[0].securityContext
171
- expect(securityContext.runAsNonRoot).toBe(true)
172
- expect(securityContext.allowPrivilegeEscalation).toBe(false)
173
- expect(securityContext.seccompProfile).toEqual({ type: 'RuntimeDefault' })
174
- expect(securityContext.capabilities.drop).toEqual(['ALL'])
175
- })
176
-
177
- it('gives the sandbox explicit resources by default and honours overrides', () => {
178
- const defaults = renderPaperclip({ host: 'paperclip.example.com', agents: {} })
179
- const defaultSandbox = defaults.resources.find(
180
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
181
- ) as any
182
- const defaultContainer = defaultSandbox.spec.template.spec.containers[0]
183
- expect(defaultContainer.resources.requests).toEqual({ cpu: '250m', memory: '256Mi' })
184
- expect(defaultContainer.resources.limits).toEqual({ cpu: '1000m', memory: '2Gi' })
185
-
186
- const result = renderPaperclip({
187
- host: 'paperclip.example.com',
188
- agents: {
189
- sandboxReplicas: 1,
190
- resources: {
191
- requests: { cpu: '500m', memory: '1Gi' },
192
- limits: { cpu: '2000m', memory: '4Gi' },
193
- },
194
- },
195
- })
196
- const sandbox = result.resources.find(
197
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
198
- ) as any
199
- const container = sandbox.spec.template.spec.containers[0]
200
- expect(container.resources.requests).toEqual({ cpu: '500m', memory: '1Gi' })
201
- expect(container.resources.limits).toEqual({ cpu: '2000m', memory: '4Gi' })
202
- })
203
-
204
- it('defaults sandbox replicas to 2', () => {
205
- const result = renderPaperclip({ host: 'paperclip.example.com', agents: {} })
206
- const sandbox = result.resources.find(
207
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
208
- ) as any
209
- expect(sandbox.spec.replicas).toBe(2)
210
- })
211
-
212
- it('renders gateway resources when platform uses gateway routing', () => {
213
- const result = render(
214
- jsx(RoutingContext.Provider, {
215
- value: { mode: 'gateway', gatewayClassName: 'eg' },
216
- children: jsx(SecretContext.Provider, {
217
- value: openbao as never,
218
- children: jsx(Paperclip, { host: 'paperclip.example.com' }),
219
- }),
220
- })
221
- )
222
- const kinds = result.resources.map((r) => r.kind)
223
- expect(kinds).toContain('HTTPRoute')
224
- })
225
-
226
- it('renders a valid Ingress when platform uses ingress routing', () => {
227
- const result = render(
228
- jsx(RoutingContext.Provider, {
229
- value: { mode: 'ingress' },
230
- children: jsx(SecretContext.Provider, {
231
- value: openbao as never,
232
- children: jsx(Paperclip, { host: 'paperclip.example.com' }),
233
- }),
234
- })
235
- )
236
- const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
237
- expect(ingress).toBeDefined()
238
- expect(ingress.spec.rules[0].host).toBe('paperclip.example.com')
239
- })
240
-
241
- it('passes resource validation', () => {
242
- const result = renderPaperclip({
243
- host: 'paperclip.example.com',
244
- agents: { sandboxReplicas: 2 },
245
- })
246
- for (const resource of result.resources) {
247
- expect(validateResource(resource)).toEqual([])
248
- }
249
- })
250
- })
251
-
252
- describe('namespace inheritance', () => {
253
- it('inherits namespace from the Platform context when namespace prop is not set', () => {
254
- const result = renderPaperclipInNamespace('agents', {
255
- host: 'paperclip.example.com',
256
- agents: { sandboxReplicas: 1 },
257
- })
258
- for (const kind of ['Deployment', 'Service', 'Cluster', 'Ingress']) {
259
- const resource = result.resources.find((r: any) => r.kind === kind)
260
- expect(resource).toBeDefined()
261
- expect(resource.metadata.namespace).toBe('agents')
262
- }
263
- })
264
-
265
- it('explicit namespace prop wins over the Platform context', () => {
266
- const result = renderPaperclipInNamespace('agents', {
267
- host: 'paperclip.example.com',
268
- namespace: 'paperclip-ns',
269
- })
270
- const main = result.resources.find(
271
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip'
272
- ) as any
273
- expect(main.metadata.namespace).toBe('paperclip-ns')
274
- })
275
-
276
- it('falls back to default when no Platform namespace is present', () => {
277
- const result = renderPaperclip({ host: 'paperclip.example.com' })
278
- const main = result.resources.find(
279
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip'
280
- ) as any
281
- expect(main.metadata.namespace).toBe('default')
282
- })
283
- })
284
-
285
- describe('rendering with all props', () => {
286
- it('accepts the full prop surface', () => {
287
- const result = renderPaperclip({
288
- name: 'agents',
289
- namespace: 'agents',
290
- version: '0.4.0',
291
- host: 'agents.example.com',
292
- replicas: 3,
293
- dbStorage: '20Gi',
294
- websockets: true,
295
- agents: { sandboxReplicas: 4 },
296
- resources: {
297
- requests: { memory: '1Gi', cpu: '500m' },
298
- limits: { memory: '4Gi', cpu: '2000m' },
299
- },
300
- tls: { secretName: 'agents-tls', clusterIssuer: 'letsencrypt-prod' },
301
- })
302
- expect(result.resources.length).toBeGreaterThan(0)
303
-
304
- const main = result.resources.find(
305
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'agents'
306
- ) as any
307
- const container = main.spec.template.spec.containers[0]
308
- expect(container.image).toBe('ghcr.io/berget-ai/paperclip:0.4.0')
309
- expect(main.spec.replicas).toBe(3)
310
- expect(container.resources.limits.memory).toBe('4Gi')
311
- const websockets = container.env.find((e: any) => e.name === 'WEBSOCKETS_ENABLED')
312
- expect(websockets.value).toBe('true')
313
-
314
- const sandbox = result.resources.find(
315
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'agents-agent-sandbox'
316
- ) as any
317
- expect(sandbox.spec.replicas).toBe(4)
318
-
319
- const cluster = result.resources.find((r) => r.kind === 'Cluster') as any
320
- expect(cluster.spec.storage.size).toBe('20Gi')
321
- })
322
-
323
- it('renders unique env var names (k8s rejects duplicates)', () => {
324
- const result = renderPaperclip({
325
- host: 'paperclip.example.com',
326
- agents: { sandboxReplicas: 1 },
327
- })
328
- for (const name of ['paperclip', 'paperclip-agent-sandbox']) {
329
- const deployment = result.resources.find(
330
- (r: any) => r.kind === 'Deployment' && r.metadata.name === name
331
- ) as any
332
- const env = deployment.spec.template.spec.containers[0].env as Array<{ name: string }>
333
- const names = env.map((e) => e.name)
334
- expect(new Set(names).size).toBe(names.length)
335
- }
336
- })
337
- })
338
-
339
- describe('secrets handling', () => {
340
- it('provisions the model API key through a secrets backend', () => {
341
- const result = renderPaperclip({ host: 'paperclip.example.com' })
342
- const bundle = result.resources.find(
343
- (r: any) => r.kind === 'OpenBaoStaticSecret' && r.metadata.name === 'paperclip-secrets'
344
- ) as any
345
- expect(bundle).toBeDefined()
346
- expect(bundle.spec.path).toBe('test/paperclip/secrets')
347
- expect(bundle.spec.destination).toEqual({
348
- create: true,
349
- name: 'paperclip-secrets',
350
- })
351
- })
352
-
353
- it('provisions the model API key through Vault', () => {
354
- const result = render(
355
- jsx(SecretContext.Provider, {
356
- value: { backend: 'vault', mount: 'kv', path: 'apps' },
357
- children: jsx(Paperclip, { host: 'paperclip.example.com' }),
358
- })
359
- )
360
- const kinds = result.resources.map((r) => r.kind)
361
- expect(kinds).toContain('VaultStaticSecret')
362
- })
363
-
364
- it('accepts an explicit secretsName without a backend', () => {
365
- const result = render(
366
- jsx(Paperclip, { host: 'paperclip.example.com', secretsName: 'existing-secrets' })
367
- )
368
- const kinds = result.resources.map((r) => r.kind)
369
- expect(kinds).not.toContain('OpenBaoStaticSecret')
370
- expect(kinds).not.toContain('VaultStaticSecret')
371
- expect(result.resources.length).toBeGreaterThan(0)
372
- })
373
-
374
- it('wires credentials via secretKeyRef (never plaintext env)', () => {
375
- const result = render(
376
- jsx(Paperclip, { host: 'paperclip.example.com', secretsName: 'existing-secrets' })
377
- )
378
- const main = result.resources.find(
379
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip'
380
- ) as any
381
- const env = main.spec.template.spec.containers[0].env
382
- const modelApiKey = env.find((e: any) => e.name === 'MODEL_API_KEY')
383
- const dbPassword = env.find((e: any) => e.name === 'PGPASSWORD')
384
- expect(modelApiKey.valueFrom.secretKeyRef).toEqual({
385
- name: 'existing-secrets',
386
- key: 'modelApiKey',
387
- })
388
- expect(dbPassword.valueFrom.secretKeyRef).toEqual({
389
- name: 'paperclip-db-credentials',
390
- key: 'password',
391
- })
392
- expect(modelApiKey.value).toBeUndefined()
393
- expect(dbPassword.value).toBeUndefined()
394
- const databaseUrl = env.find((e: any) => e.name === 'DATABASE_URL')
395
- expect(databaseUrl.value).toContain('$(PGPASSWORD)')
396
- })
397
-
398
- it('sandbox workers share the same secrets bundle', () => {
399
- const result = render(
400
- jsx(SecretContext.Provider, {
401
- value: openbao as never,
402
- children: jsx(Paperclip, {
403
- host: 'paperclip.example.com',
404
- agents: { sandboxReplicas: 2 },
405
- }),
406
- })
407
- )
408
- const sandbox = result.resources.find(
409
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'paperclip-agent-sandbox'
410
- ) as any
411
- const env = sandbox.spec.template.spec.containers[0].env
412
- const modelApiKey = env.find((e: any) => e.name === 'MODEL_API_KEY')
413
- const dbPassword = env.find((e: any) => e.name === 'PGPASSWORD')
414
- expect(modelApiKey.valueFrom.secretKeyRef.name).toBe('paperclip-secrets')
415
- expect(modelApiKey.valueFrom.secretKeyRef.key).toBe('modelApiKey')
416
- expect(dbPassword.valueFrom.secretKeyRef.name).toBe('paperclip-db-credentials')
417
- expect(modelApiKey.value).toBeUndefined()
418
- })
419
-
420
- it('renders no plaintext credentials anywhere', () => {
421
- const result = renderPaperclip({
422
- host: 'paperclip.example.com',
423
- agents: { sandboxReplicas: 2 },
424
- })
425
- const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
426
- if (!passed) {
427
- console.error('Plaintext credential violations:', errors)
428
- }
429
- expect(passed).toBe(true)
430
- })
431
- })
432
-
433
- describe('validation errors', () => {
434
- it('throws when no secrets backend and no model key secret', () => {
435
- expect(() => render(jsx(Paperclip, { host: 'paperclip.example.com' }))).toThrow(
436
- /model api key/i
437
- )
438
- })
439
-
440
- it('throws for unknown secrets backends', () => {
441
- expect(() =>
442
- render(
443
- jsx(SecretContext.Provider, {
444
- value: { backend: 'unknown' as never },
445
- children: jsx(Paperclip, { host: 'paperclip.example.com' }),
446
- })
447
- )
448
- ).toThrow(/model api key/i)
449
- })
450
- })
@@ -1,8 +0,0 @@
1
- import { Platform } from '@r8s/recipes'
2
- import { Paperclip } from '@r8s/paperclip'
3
-
4
- export default (
5
- <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
6
- <Paperclip name="paperclip" host="paperclip.example.com" agents={{ sandboxReplicas: 3 }} />
7
- </Platform>
8
- )
package/src/index.tsx DELETED
@@ -1,278 +0,0 @@
1
- import { jsx, Fragment, useContext } from '@r8s/core'
2
- import { Namespace, SecretContext } from '@r8s/core/defaults'
3
- import { Database, WebService, Endpoint } from '@r8s/recipes'
4
- import type { SecretRef } from '@r8s/recipes'
5
-
6
- export interface PaperclipProps {
7
- /** Resource name (defaults to 'paperclip') */
8
- name?: string
9
- /** Kubernetes namespace (defaults to 'default') */
10
- namespace?: string
11
- /** Container image tag (defaults to 'latest' — pin a version in production) */
12
- version?: string
13
- /** Public hostname for the web app and API (required) */
14
- host: string
15
- /** Number of app replicas (defaults to 2) */
16
- replicas?: number
17
- /** Storage size for the Postgres cluster (defaults to '10Gi') */
18
- dbStorage?: string
19
- /** Enable websockets for live task and agent updates (defaults to false) */
20
- websockets?: boolean
21
- /**
22
- * Sandbox agent workers. Workers run the same image with a command
23
- * override (paperclip agent --sandbox) and share the model API key
24
- * and database credentials via secretKeyRef. They run with a hardened
25
- * securityContext (non-root, no privilege escalation, RuntimeDefault
26
- * seccomp, all capabilities dropped) by default.
27
- */
28
- agents?: {
29
- /** Number of sandbox agent replicas (defaults to 2) */
30
- sandboxReplicas?: number
31
- /** Sandbox container resources (defaults to requests 256Mi/250m, limits 2Gi/1000m) */
32
- resources?: {
33
- requests?: { cpu?: string; memory?: string }
34
- limits?: { cpu?: string; memory?: string }
35
- }
36
- }
37
- /**
38
- * Name of an existing Secret containing key `modelApiKey`. Paperclip
39
- * uses this key to call LLM providers on behalf of agents.
40
- *
41
- * Required unless a secrets backend (openbao/vault) is configured on
42
- * the surrounding Platform — the backend then provisions the key
43
- * automatically. Plaintext keys are not supported.
44
- */
45
- secretsName?: string
46
- /** Requested app resources */
47
- resources?: {
48
- requests?: { cpu?: string; memory?: string }
49
- limits?: { cpu?: string; memory?: string }
50
- }
51
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
52
- tls?: {
53
- secretName: string
54
- clusterIssuer: string
55
- }
56
- }
57
-
58
- /**
59
- * Paperclip — Berget's agent platform (tasks, documents, agent orchestration).
60
- *
61
- * @title Paperclip
62
- * @category Agent Platforms
63
- *
64
- * Composes:
65
- * - CNPG Postgres cluster (tasks, documents, agent state; size via
66
- * `dbStorage`)
67
- * - Paperclip Deployment + Service + Endpoint
68
- * - Optional sandbox agent workers (same image, command override) sharing
69
- * the model API key via secretKeyRef — hardened with a non-root,
70
- * no-privilege-escalation securityContext, RuntimeDefault seccomp and
71
- * all capabilities dropped, with explicit resources by default
72
- * - Model API key provisioned through the Platform secrets backend
73
- * (openbao / vault), or referenced from an existing Secret
74
- * - Optional websockets support (WEBSOCKETS_ENABLED)
75
- *
76
- * Probing: the Paperclip image contract does not publish HTTP health
77
- * endpoints, so both workloads default to TCP socket probes on the
78
- * container port (3000) to avoid crash-looping on unknown /health /ready
79
- * paths. If your image serves HTTP /health and /ready — or another
80
- * readiness endpoint — override `probes` on the workload for stricter
81
- * checks.
82
- *
83
- * The namespace is inherited from the surrounding `<Platform>` (via the
84
- * Namespace context) unless set explicitly.
85
- *
86
- * Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
87
- * the MODEL_API_KEY is provisioned for you. Without a backend you must
88
- * point `secretsName` at a pre-created Secret.
89
- *
90
- * @example
91
- * import { Platform } from '@r8s/recipes'
92
- * import { Paperclip } from '@r8s/paperclip'
93
- *
94
- * export default (
95
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
96
- * <Paperclip
97
- * name="paperclip"
98
- * host="paperclip.example.com"
99
- * agents={{ sandboxReplicas: 3 }}
100
- * />
101
- * </Platform>
102
- * )
103
- *
104
- * @example
105
- * // Existing Secret holding the model API key (key: modelApiKey)
106
- * import { Paperclip } from '@r8s/paperclip'
107
- *
108
- * export default (
109
- * <Paperclip name="paperclip" host="paperclip.example.com" secretsName="paperclip-secrets" />
110
- * )
111
- */
112
- export function Paperclip(props: PaperclipProps) {
113
- const {
114
- name = 'paperclip',
115
- namespace: namespaceProp,
116
- version = 'latest',
117
- host,
118
- replicas = 2,
119
- dbStorage = '10Gi',
120
- websockets = false,
121
- agents,
122
- secretsName,
123
- resources = {
124
- requests: { memory: '512Mi', cpu: '250m' },
125
- limits: { memory: '2Gi', cpu: '1000m' },
126
- },
127
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
128
- } = props
129
-
130
- // Inherit namespace from <Platform> context if not explicitly set
131
- const contextNamespace = useContext(Namespace)
132
- const namespace =
133
- namespaceProp ?? (contextNamespace !== 'default' ? contextNamespace : undefined) ?? 'default'
134
-
135
- const secretProvider = useContext(SecretContext)
136
- const resources_: ReturnType<typeof jsx>[] = []
137
-
138
- const image = `ghcr.io/berget-ai/paperclip:${version}`
139
- const appSecretsName = secretsName ?? `${name}-secrets`
140
- const sandboxReplicas = agents?.sandboxReplicas ?? 2
141
- // Sandbox workers run untrusted code — never let them float without
142
- // explicit container resources.
143
- const sandboxResources = agents?.resources ?? {
144
- requests: { cpu: '250m', memory: '256Mi' },
145
- limits: { cpu: '1000m', memory: '2Gi' },
146
- }
147
- // The internal image's probe contract is unknown — probe the TCP port
148
- // so pods don't crash-loop on missing HTTP /health /ready endpoints.
149
- const tcpProbes = {
150
- liveness: { tcp: true } as const,
151
- readiness: { tcp: true } as const,
152
- }
153
-
154
- // --- Secret provisioning -------------------------------------------------
155
- // The model API key lets Paperclip call LLM providers — never render
156
- // it as plaintext. With a secrets backend it is provisioned through the
157
- // backend (key `modelApiKey`); otherwise reference a pre-created Secret.
158
- if (!secretsName) {
159
- if (
160
- !secretProvider ||
161
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
162
- ) {
163
- throw new Error(
164
- `Paperclip "${name}" requires a model API key (MODEL_API_KEY).\n` +
165
- `\n` +
166
- `Paperclip calls LLM providers with a model API key — ` +
167
- `it must not be rendered as plaintext.\n` +
168
- `\n` +
169
- `Fix: configure a secrets backend on the Platform:\n` +
170
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>\n` +
171
- ` <Paperclip name="${name}" host="${host}" />\n` +
172
- ` </Platform>\n` +
173
- `\n` +
174
- `Or reference a pre-created Secret (key: modelApiKey):\n` +
175
- ` <Paperclip name="${name}" host="${host}" secretsName="${name}-secrets" />`
176
- )
177
- }
178
-
179
- const spec = {
180
- ...(secretProvider.backend === 'vault'
181
- ? { vaultAuthRef: secretProvider.authRef }
182
- : { openbaoAuthRef: secretProvider.authRef }),
183
- mount: secretProvider.mount,
184
- type: 'kv-v2' as const,
185
- path: `${secretProvider.path ?? name}/${name}/secrets`,
186
- destination: { create: true, name: appSecretsName },
187
- }
188
- resources_.push(
189
- secretProvider.backend === 'vault'
190
- ? jsx('VaultStaticSecret', {
191
- apiVersion: 'secrets.hashicorp.com/v1beta1',
192
- kind: 'VaultStaticSecret',
193
- metadata: { name: `${name}-secrets`, namespace },
194
- spec,
195
- })
196
- : jsx('OpenBaoStaticSecret', {
197
- apiVersion: 'secrets.openbao.org/v1beta1',
198
- kind: 'OpenBaoStaticSecret',
199
- metadata: { name: `${name}-secrets`, namespace },
200
- spec,
201
- })
202
- )
203
- }
204
-
205
- // --- Env wiring ------------------------------------------------------------
206
- // Every credential is referenced via secretKeyRef (declared before plain
207
- // env vars by WebService, so dependent $(VAR) expansion resolves).
208
- // DATABASE_URL is auto-wired from DatabaseContext inside the wrapper.
209
- const appEnv: Record<string, string> = {
210
- PORT: '3000',
211
- ...(websockets ? { WEBSOCKETS_ENABLED: 'true' } : {}),
212
- }
213
-
214
- const appSecrets: Record<string, SecretRef> = {
215
- MODEL_API_KEY: { secret: appSecretsName, key: 'modelApiKey' },
216
- }
217
-
218
- // --- Database (CNPG) + app + sandbox agents --------------------------------
219
- // Database wraps both the main app and the sandbox workers so database
220
- // credentials stay consistent with the r8s Database recipe (CNPG dedicated
221
- // cluster provisions the secret).
222
- resources_.push(
223
- jsx(Database, {
224
- name,
225
- namespace,
226
- storage: dbStorage,
227
- children: (
228
- <>
229
- <WebService
230
- name={name}
231
- namespace={namespace}
232
- image={image}
233
- port={3000}
234
- replicas={replicas}
235
- probes={tcpProbes}
236
- resources={resources}
237
- env={appEnv}
238
- secrets={appSecrets}
239
- />
240
- {agents ? (
241
- <WebService
242
- name={`${name}-agent-sandbox`}
243
- namespace={namespace}
244
- image={image}
245
- port={3000}
246
- replicas={sandboxReplicas}
247
- command={['paperclip', 'agent', '--sandbox']}
248
- probes={tcpProbes}
249
- secrets={appSecrets}
250
- securityContext={{
251
- runAsNonRoot: true,
252
- allowPrivilegeEscalation: false,
253
- seccompProfile: { type: 'RuntimeDefault' },
254
- capabilities: { drop: ['ALL'] },
255
- }}
256
- podSecurityContext={{ seccompProfile: { type: 'RuntimeDefault' } }}
257
- resources={sandboxResources}
258
- />
259
- ) : null}
260
- </>
261
- ),
262
- })
263
- )
264
-
265
- // --- Endpoint ---------------------------------------------------------------
266
- resources_.push(
267
- <Endpoint
268
- name={`${name}-endpoint`}
269
- namespace={namespace}
270
- host={host}
271
- serviceName={name}
272
- servicePort={3000}
273
- tls={tls}
274
- />
275
- )
276
-
277
- return jsx(Fragment, { children: resources_ })
278
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": true
7
- },
8
- "include": ["src/**/*"],
9
- "references": [
10
- { "path": "../core" },
11
- { "path": "../k8s-types" },
12
- { "path": "../crds" },
13
- { "path": "../recipes" }
14
- ]
15
- }