thinkwork-cli 0.13.2 → 1.0.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 +3 -14
- package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
- package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
- package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
- package/dist/chunk-MWQ7RO76.js +137 -0
- package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
- package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
- package/dist/cli.js +4400 -16431
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
- package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
- package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
- package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
- package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
- package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
- package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
- package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
- package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
- package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
- package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
- package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
- package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
- package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
- package/dist/drizzle/0230_analyst_rls.sql +28 -45
- package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
- package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
- package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
- package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
- package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
- package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
- package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
- package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
- package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
- package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
- package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
- package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
- package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
- package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
- package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
- package/dist/drizzle/0284_user_brain_claims.sql +69 -0
- package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
- package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
- package/dist/drizzle/0287_brain_subject.sql +25 -0
- package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
- package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
- package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
- package/dist/plugins/README.md +2 -2
- package/dist/plugins/catalog/package.json +0 -3
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
- package/dist/plugins/catalog/src/contracts.ts +15 -0
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
- package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
- package/dist/plugins/twenty/package.json +2 -1
- package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
- package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
- package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
- package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
- package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
- package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
- package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
- package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
- package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
- package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
- package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
- package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
- package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
- package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
- package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
- package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
- package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
- package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
- package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
- package/dist/plugins/twenty/test/manifest.test.ts +2 -4
- package/dist/scripts/post-deploy.sh +79 -1
- package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
- package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
- package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
- package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
- package/dist/terraform/examples/greenfield/main.tf +233 -175
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
- package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
- package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
- package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
- package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
- package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
- package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
- package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
- package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
- package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
- package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
- package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
- package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
- package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
- package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
- package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
- package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
- package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
- package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
- package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
- package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
- package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
- package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
- package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
- package/dist/terraform/modules/app/static-site/main.tf +1 -1
- package/dist/terraform/modules/app/www-dns/main.tf +9 -27
- package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
- package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
- package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
- package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
- package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
- package/dist/terraform/modules/thinkwork/README.md +5 -1
- package/dist/terraform/modules/thinkwork/main.tf +376 -374
- package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
- package/dist/terraform/modules/thinkwork/variables.tf +246 -89
- package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
- package/dist/workspace-defaults/files/ROUTER.md +1 -1
- package/dist/workspace-defaults/files/TOOLS.md +9 -39
- package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
- package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
- package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
- package/package.json +1 -1
- package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
- package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
- package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
- package/dist/plugins/company-data/README.md +0 -49
- package/dist/plugins/company-data/package.json +0 -21
- package/dist/plugins/company-data/src/index.ts +0 -30
- package/dist/plugins/company-data/src/manifest.ts +0 -26
- package/dist/plugins/company-data/test/manifest.test.ts +0 -116
- package/dist/plugins/company-data/tsconfig.json +0 -8
- package/dist/plugins/company-etl/README.md +0 -46
- package/dist/plugins/company-etl/package.json +0 -21
- package/dist/plugins/company-etl/src/index.ts +0 -29
- package/dist/plugins/company-etl/src/manifest.ts +0 -26
- package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
- package/dist/plugins/company-etl/tsconfig.json +0 -8
- package/dist/plugins/lastmile/README.md +0 -50
- package/dist/plugins/lastmile/package.json +0 -23
- package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
- package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
- package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
- package/dist/plugins/lastmile/src/index.ts +0 -46
- package/dist/plugins/lastmile/src/manifest.ts +0 -139
- package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
- package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
- package/dist/plugins/lastmile/tsconfig.json +0 -8
- package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
- package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
- package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
- package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
- package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
- package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
- package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
- package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
- package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
- package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
- package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
- package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
- package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
- package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
- package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
- package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Smoke test the Twenty CRM managed application deployment.
|
|
4
|
-
*
|
|
5
|
-
* Dry-run is the default. Set SMOKE_ENABLE_TWENTY_MANAGED_APP=1 after deploy
|
|
6
|
-
* to read Terraform/API deployment status and probe the public Twenty /healthz
|
|
7
|
-
* endpoint when the runtime is enabled.
|
|
8
|
-
*
|
|
9
|
-
* Optional live env:
|
|
10
|
-
* SMOKE_TERRAFORM_DIR=terraform/examples/greenfield
|
|
11
|
-
* SMOKE_TWENTY_URL=https://crm.example.com
|
|
12
|
-
* SMOKE_TENANT_ID=<tenant-id>
|
|
13
|
-
* VITE_GRAPHQL_HTTP_URL, GRAPHQL_HTTP_URL, or API_GRAPHQL_URL
|
|
14
|
-
* API_AUTH_SECRET, THINKWORK_API_SECRET, VITE_GRAPHQL_API_KEY, or GRAPHQL_API_KEY
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { execFileSync } from "node:child_process";
|
|
18
|
-
import fs from "node:fs";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import process from "node:process";
|
|
21
|
-
import { attachSmokeEvidence } from "../../../scripts/smoke/deployment-evidence.mjs";
|
|
22
|
-
|
|
23
|
-
const LIVE_ENABLED = process.env.SMOKE_ENABLE_TWENTY_MANAGED_APP === "1";
|
|
24
|
-
const TIMEOUT_MS = Number(process.env.SMOKE_TIMEOUT_MS || 15_000);
|
|
25
|
-
|
|
26
|
-
const env = {
|
|
27
|
-
...loadEnvFile(),
|
|
28
|
-
...process.env,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const terraformDir = env.SMOKE_TERRAFORM_DIR || "terraform/examples/greenfield";
|
|
32
|
-
const apiUrl = first(
|
|
33
|
-
env.VITE_GRAPHQL_HTTP_URL,
|
|
34
|
-
env.GRAPHQL_HTTP_URL,
|
|
35
|
-
env.API_GRAPHQL_URL,
|
|
36
|
-
);
|
|
37
|
-
const apiSecret = first(env.API_AUTH_SECRET, env.THINKWORK_API_SECRET);
|
|
38
|
-
const apiKey = first(
|
|
39
|
-
env.VITE_GRAPHQL_API_KEY,
|
|
40
|
-
env.APPSYNC_API_KEY,
|
|
41
|
-
env.GRAPHQL_API_KEY,
|
|
42
|
-
);
|
|
43
|
-
const tenantId = first(env.SMOKE_TENANT_ID, env.TENANT_ID);
|
|
44
|
-
|
|
45
|
-
if (!LIVE_ENABLED) {
|
|
46
|
-
console.log(
|
|
47
|
-
JSON.stringify(
|
|
48
|
-
await attachSmokeEvidence(
|
|
49
|
-
"twenty-managed-app",
|
|
50
|
-
{
|
|
51
|
-
ok: true,
|
|
52
|
-
skippedLive: true,
|
|
53
|
-
reason:
|
|
54
|
-
"set SMOKE_ENABLE_TWENTY_MANAGED_APP=1 to run the deployed Twenty managed-app smoke",
|
|
55
|
-
dryRun: {
|
|
56
|
-
terraformDir,
|
|
57
|
-
requiredWhenRunning: [
|
|
58
|
-
"Terraform outputs in SMOKE_TERRAFORM_DIR or SMOKE_TWENTY_URL",
|
|
59
|
-
"Twenty provisioned/runtime deployment status",
|
|
60
|
-
],
|
|
61
|
-
optionalGraphqlEnv: [
|
|
62
|
-
"SMOKE_TENANT_ID",
|
|
63
|
-
"VITE_GRAPHQL_HTTP_URL or GRAPHQL_HTTP_URL or API_GRAPHQL_URL",
|
|
64
|
-
"API_AUTH_SECRET or THINKWORK_API_SECRET or VITE_GRAPHQL_API_KEY or GRAPHQL_API_KEY",
|
|
65
|
-
],
|
|
66
|
-
verifies: [
|
|
67
|
-
"twenty_provisioned false skips cleanly",
|
|
68
|
-
"twenty_runtime_enabled true requires HTTPS twenty_url",
|
|
69
|
-
"public Twenty /healthz responds successfully",
|
|
70
|
-
"optional GraphQL deployment status and managed-app health agree with the public URL",
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
env,
|
|
75
|
-
),
|
|
76
|
-
null,
|
|
77
|
-
2,
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
process.exit(0);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await runLiveSmoke();
|
|
85
|
-
console.log(
|
|
86
|
-
JSON.stringify(
|
|
87
|
-
await attachSmokeEvidence(
|
|
88
|
-
"twenty-managed-app",
|
|
89
|
-
{ ok: true, ...result },
|
|
90
|
-
env,
|
|
91
|
-
),
|
|
92
|
-
null,
|
|
93
|
-
2,
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function runLiveSmoke() {
|
|
101
|
-
const terraform = readTerraformOutputs(terraformDir);
|
|
102
|
-
const graphql = await readGraphqlStatus().catch((error) => ({
|
|
103
|
-
error: error instanceof Error ? error.message : String(error),
|
|
104
|
-
}));
|
|
105
|
-
const status = resolveStatus({ terraform, graphql });
|
|
106
|
-
|
|
107
|
-
if (!status.provisioned) {
|
|
108
|
-
return {
|
|
109
|
-
skippedLive: true,
|
|
110
|
-
reason: "Twenty CRM is not provisioned for this stage.",
|
|
111
|
-
terraform,
|
|
112
|
-
graphql,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!status.runtimeEnabled) {
|
|
117
|
-
return {
|
|
118
|
-
skippedLive: true,
|
|
119
|
-
reason: "Twenty CRM is provisioned but runtime is parked.",
|
|
120
|
-
status,
|
|
121
|
-
terraform,
|
|
122
|
-
graphql,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!status.url) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
"Twenty runtime is enabled but no public URL was resolved.",
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
if (!status.url.startsWith("https://")) {
|
|
132
|
-
throw new Error(`Twenty URL must be HTTPS, got ${status.url}.`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const publicHealth = await probeHealthz(status.url);
|
|
136
|
-
const graphqlHealth =
|
|
137
|
-
graphql?.deploymentStatus && !graphql.error
|
|
138
|
-
? await readGraphqlHealth().catch((error) => ({
|
|
139
|
-
error: error instanceof Error ? error.message : String(error),
|
|
140
|
-
}))
|
|
141
|
-
: null;
|
|
142
|
-
|
|
143
|
-
if (!publicHealth.healthy) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Twenty /healthz failed: HTTP ${publicHealth.statusCode ?? "none"} ${publicHealth.message}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (graphqlHealth && "healthy" in graphqlHealth && !graphqlHealth.healthy) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`GraphQL managed health reported unhealthy: ${graphqlHealth.message}`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
status,
|
|
156
|
-
publicHealth,
|
|
157
|
-
graphqlHealth,
|
|
158
|
-
terraform,
|
|
159
|
-
graphql,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resolveStatus({ terraform, graphql }) {
|
|
164
|
-
const app = graphql?.deploymentStatus?.managedApplications?.find(
|
|
165
|
-
(entry) => entry.key === "twenty",
|
|
166
|
-
);
|
|
167
|
-
return {
|
|
168
|
-
provisioned: bool(
|
|
169
|
-
firstDefined(
|
|
170
|
-
app?.provisioned,
|
|
171
|
-
terraform.twenty_provisioned,
|
|
172
|
-
!!env.SMOKE_TWENTY_URL,
|
|
173
|
-
),
|
|
174
|
-
),
|
|
175
|
-
runtimeEnabled: bool(
|
|
176
|
-
firstDefined(
|
|
177
|
-
app?.runtimeEnabled,
|
|
178
|
-
terraform.twenty_runtime_enabled,
|
|
179
|
-
!!env.SMOKE_TWENTY_URL,
|
|
180
|
-
),
|
|
181
|
-
),
|
|
182
|
-
url: first(env.SMOKE_TWENTY_URL, app?.url, terraform.twenty_url),
|
|
183
|
-
source: app ? "graphql" : terraform.available ? "terraform" : "env",
|
|
184
|
-
status: app?.status ?? null,
|
|
185
|
-
message: app?.message ?? null,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function probeHealthz(baseUrl) {
|
|
190
|
-
const endpoint = new URL("/healthz", baseUrl).toString();
|
|
191
|
-
const started = Date.now();
|
|
192
|
-
const controller = new AbortController();
|
|
193
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
194
|
-
try {
|
|
195
|
-
const response = await fetch(endpoint, { signal: controller.signal });
|
|
196
|
-
const body = await response.text().catch(() => "");
|
|
197
|
-
return {
|
|
198
|
-
endpoint,
|
|
199
|
-
healthy: response.ok,
|
|
200
|
-
statusCode: response.status,
|
|
201
|
-
latencyMs: Date.now() - started,
|
|
202
|
-
message: body.slice(0, 300),
|
|
203
|
-
};
|
|
204
|
-
} finally {
|
|
205
|
-
clearTimeout(timer);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function readTerraformOutputs(dir) {
|
|
210
|
-
const resolved = path.resolve(dir);
|
|
211
|
-
if (!fs.existsSync(resolved)) {
|
|
212
|
-
return { available: false, reason: `Terraform dir not found: ${dir}` };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const raw = execFileSync(
|
|
217
|
-
"terraform",
|
|
218
|
-
["-chdir=" + resolved, "output", "-json"],
|
|
219
|
-
{
|
|
220
|
-
encoding: "utf8",
|
|
221
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
222
|
-
},
|
|
223
|
-
);
|
|
224
|
-
const parsed = JSON.parse(raw);
|
|
225
|
-
return {
|
|
226
|
-
available: true,
|
|
227
|
-
twenty_provisioned: outputValue(parsed.twenty_provisioned),
|
|
228
|
-
twenty_runtime_enabled: outputValue(parsed.twenty_runtime_enabled),
|
|
229
|
-
twenty_url: outputValue(parsed.twenty_url),
|
|
230
|
-
twenty_server_log_group_name: outputValue(
|
|
231
|
-
parsed.twenty_server_log_group_name,
|
|
232
|
-
),
|
|
233
|
-
twenty_worker_log_group_name: outputValue(
|
|
234
|
-
parsed.twenty_worker_log_group_name,
|
|
235
|
-
),
|
|
236
|
-
twenty_cache_endpoint: outputValue(parsed.twenty_cache_endpoint),
|
|
237
|
-
};
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return {
|
|
240
|
-
available: false,
|
|
241
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async function readGraphqlStatus() {
|
|
247
|
-
if (!apiUrl || (!apiSecret && !apiKey)) return null;
|
|
248
|
-
const data = await gql(`
|
|
249
|
-
query TwentySmokeDeploymentStatus {
|
|
250
|
-
deploymentStatus {
|
|
251
|
-
stage
|
|
252
|
-
region
|
|
253
|
-
twentyProvisioned
|
|
254
|
-
twentyRuntimeEnabled
|
|
255
|
-
twentyUrl
|
|
256
|
-
managedApplications {
|
|
257
|
-
key
|
|
258
|
-
status
|
|
259
|
-
provisioned
|
|
260
|
-
runtimeEnabled
|
|
261
|
-
url
|
|
262
|
-
message
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
`);
|
|
267
|
-
return data;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function readGraphqlHealth() {
|
|
271
|
-
const data = await gql(`
|
|
272
|
-
query TwentySmokeManagedApplicationHealth {
|
|
273
|
-
managedApplicationHealthCheck(key: "twenty") {
|
|
274
|
-
healthy
|
|
275
|
-
statusCode
|
|
276
|
-
latencyMs
|
|
277
|
-
endpoint
|
|
278
|
-
message
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
`);
|
|
282
|
-
return data.managedApplicationHealthCheck;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function gql(query) {
|
|
286
|
-
const headers = { "content-type": "application/json" };
|
|
287
|
-
if (tenantId) headers["x-tenant-id"] = tenantId;
|
|
288
|
-
if (apiSecret) headers.authorization = `Bearer ${apiSecret}`;
|
|
289
|
-
else headers["x-api-key"] = apiKey;
|
|
290
|
-
|
|
291
|
-
const response = await fetch(apiUrl, {
|
|
292
|
-
method: "POST",
|
|
293
|
-
headers,
|
|
294
|
-
body: JSON.stringify({ query }),
|
|
295
|
-
});
|
|
296
|
-
if (!response.ok) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`GraphQL HTTP ${response.status}: ${await response.text()}`,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
const body = await response.json();
|
|
302
|
-
if (body.errors?.length) {
|
|
303
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`);
|
|
304
|
-
}
|
|
305
|
-
return body.data;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function outputValue(entry) {
|
|
309
|
-
return entry && Object.prototype.hasOwnProperty.call(entry, "value")
|
|
310
|
-
? entry.value
|
|
311
|
-
: undefined;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function firstDefined(...values) {
|
|
315
|
-
return values.find((value) => value !== undefined && value !== null);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function bool(value) {
|
|
319
|
-
return value === true || value === "true" || value === "1";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function loadEnvFile() {
|
|
323
|
-
const explicit = process.env.COMPUTER_ENV_FILE;
|
|
324
|
-
if (explicit === "none") return {};
|
|
325
|
-
const candidates = [explicit, "apps/web/.env", ".env"].filter(Boolean);
|
|
326
|
-
|
|
327
|
-
for (const candidate of candidates) {
|
|
328
|
-
const resolved = path.resolve(candidate);
|
|
329
|
-
if (!fs.existsSync(resolved)) continue;
|
|
330
|
-
return Object.fromEntries(
|
|
331
|
-
fs
|
|
332
|
-
.readFileSync(resolved, "utf8")
|
|
333
|
-
.split(/\r?\n/)
|
|
334
|
-
.map((line) => line.trim())
|
|
335
|
-
.filter((line) => line && !line.startsWith("#") && line.includes("="))
|
|
336
|
-
.map((line) => {
|
|
337
|
-
const index = line.indexOf("=");
|
|
338
|
-
const key = line.slice(0, index).trim();
|
|
339
|
-
const value = line
|
|
340
|
-
.slice(index + 1)
|
|
341
|
-
.trim()
|
|
342
|
-
.replace(/^['"]|['"]$/g, "");
|
|
343
|
-
return [key, value];
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
return {};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function first(...values) {
|
|
351
|
-
return values.find(
|
|
352
|
-
(value) => value !== undefined && value !== null && value !== "",
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function fail(message) {
|
|
357
|
-
console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
358
|
-
process.exit(1);
|
|
359
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# Sandbox base image for the AgentCore Code Interpreter.
|
|
2
|
-
#
|
|
3
|
-
# Builds a Python 3.12 container with a small, pinned set of libraries
|
|
4
|
-
# template authors can rely on without paying the first-turn `pip install`
|
|
5
|
-
# tax. Installs sitecustomize.py so the stdout/stderr token redactor is
|
|
6
|
-
# active before any user code or framework traceback runs on cold start.
|
|
7
|
-
#
|
|
8
|
-
# Image tagging is CI-driven (see scripts/build_and_push_sandbox_base.sh).
|
|
9
|
-
# Tags are immutable; a bump ships as a reviewable PR.
|
|
10
|
-
#
|
|
11
|
-
# Build context is the repository root so the COPY line can reach the
|
|
12
|
-
# module-owned Python source below.
|
|
13
|
-
#
|
|
14
|
-
# docker build -f terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base .
|
|
15
|
-
|
|
16
|
-
FROM python:3.12-slim AS base
|
|
17
|
-
|
|
18
|
-
# ---------------------------------------------------------------------------
|
|
19
|
-
# System packages — minimal. The sandbox runs agent code, not a full distro.
|
|
20
|
-
# ---------------------------------------------------------------------------
|
|
21
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
22
|
-
ca-certificates \
|
|
23
|
-
curl \
|
|
24
|
-
git \
|
|
25
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
26
|
-
|
|
27
|
-
# ---------------------------------------------------------------------------
|
|
28
|
-
# Blessed Python libraries — pinned to explicit versions per plan R14/R24.
|
|
29
|
-
# Bumps happen in reviewable PRs. Runtime `pip install` is still allowed
|
|
30
|
-
# (per R15; T2 residual is named), but these baseline packages install once
|
|
31
|
-
# at image bake and don't pay the per-turn cost.
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
RUN pip install --no-cache-dir \
|
|
34
|
-
pandas==2.2.3 \
|
|
35
|
-
numpy==2.1.3 \
|
|
36
|
-
requests==2.32.3 \
|
|
37
|
-
boto3==1.38.0 \
|
|
38
|
-
httpx==0.28.1 \
|
|
39
|
-
pyyaml==6.0.2 \
|
|
40
|
-
openpyxl==3.1.5 \
|
|
41
|
-
python-dateutil==2.9.0.post0
|
|
42
|
-
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
# sitecustomize.py — the R13 stdout/stderr token redactor. Python
|
|
45
|
-
# auto-imports any module named "sitecustomize" on interpreter startup, so
|
|
46
|
-
# placing it in site-packages makes the redactor load before any user code
|
|
47
|
-
# (and before the framework traceback on cold start). See the module
|
|
48
|
-
# docstring for the honestly-scoped invariant and named residual gaps.
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
COPY terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py \
|
|
51
|
-
/usr/local/lib/python3.12/site-packages/sitecustomize.py
|
|
52
|
-
|
|
53
|
-
# ---------------------------------------------------------------------------
|
|
54
|
-
# Startup assertion — fail loudly if the module isn't wired up or
|
|
55
|
-
# THINKWORK_SANDBOX_SCRUBBER=off slips through to prod. Runs in the same
|
|
56
|
-
# image smoke-test CI job that pushes the tag.
|
|
57
|
-
# ---------------------------------------------------------------------------
|
|
58
|
-
RUN python -c "import sitecustomize; assert sitecustomize.installed(), 'sitecustomize did not install its stdio wrapper on import'; print('sitecustomize: OK')"
|
|
59
|
-
|
|
60
|
-
# AgentCore Code Interpreter supplies its own entrypoint; we provide the
|
|
61
|
-
# libraries + scrubber only. No CMD/ENTRYPOINT override.
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
"""sitecustomize — primary R13 token redactor for the AgentCore sandbox base image.
|
|
2
|
-
|
|
3
|
-
Python auto-imports any module named ``sitecustomize`` during interpreter
|
|
4
|
-
startup (before user code, before the preamble, before framework tracebacks on
|
|
5
|
-
cold start). This file installs a stdio write-wrapper keyed on a session-scoped
|
|
6
|
-
set of token values so no token leaks to stdout / stderr via Python-stdio
|
|
7
|
-
writes.
|
|
8
|
-
|
|
9
|
-
The invariant is honestly scoped per the brainstorm's R13 (as aligned in
|
|
10
|
-
docs/brainstorms/2026-04-22-agentcore-code-sandbox-requirements.md):
|
|
11
|
-
|
|
12
|
-
"no token value reaches a persisted log ... via Python-stdio-mediated writes"
|
|
13
|
-
|
|
14
|
-
Bytes that bypass Python stdio (``os.write(fd, ...)``, subprocesses inheriting
|
|
15
|
-
fds, C extensions, multiprocessing children) are the named residual class
|
|
16
|
-
covered by the Unit 12 CloudWatch subscription-filter backstop for known
|
|
17
|
-
OAuth prefixes, not by this module.
|
|
18
|
-
|
|
19
|
-
Public API, callable from the sandbox preamble:
|
|
20
|
-
register_token(value: str) -> None
|
|
21
|
-
register_tokens(values: Iterable[str]) -> None
|
|
22
|
-
clear_tokens() -> None
|
|
23
|
-
installed() -> bool
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import base64
|
|
29
|
-
import binascii
|
|
30
|
-
import os
|
|
31
|
-
import sys
|
|
32
|
-
import urllib.parse
|
|
33
|
-
from collections.abc import Iterable
|
|
34
|
-
from typing import Any
|
|
35
|
-
|
|
36
|
-
# ---------------------------------------------------------------------------
|
|
37
|
-
# Module state
|
|
38
|
-
# ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
# Every unique byte-sequence we redact. Populated by ``register_token`` — each
|
|
41
|
-
# registered token expands into its cleartext, standard base64, URL-safe
|
|
42
|
-
# base64, URL-encoded, and hex forms (only the forms whose length is at least
|
|
43
|
-
# MIN_TOKEN_LEN, to avoid redacting short accidental matches like "AQ").
|
|
44
|
-
_redactions: set[bytes] = set()
|
|
45
|
-
|
|
46
|
-
# Redaction placeholder. Length unimportant — stream layout survives because
|
|
47
|
-
# writes stay synchronous.
|
|
48
|
-
_REDACTION = b"<redacted>"
|
|
49
|
-
|
|
50
|
-
# Forms shorter than this are rejected — redacting 3-character strings is the
|
|
51
|
-
# shape of a wildcard bug, not a security control.
|
|
52
|
-
MIN_TOKEN_LEN = 12
|
|
53
|
-
|
|
54
|
-
# Rolling-suffix buffer size. Set at ``register_token`` time to
|
|
55
|
-
# max(existing, 2 * len(new_token_form)). The wrapper keeps this many bytes
|
|
56
|
-
# from the previous write and prepends them to the current write before
|
|
57
|
-
# searching, so a token split across two ``write()`` calls is caught as long
|
|
58
|
-
# as neither split half exceeds the buffer. Adversarial fragmentation at
|
|
59
|
-
# arbitrary positions is a named residual (see R13).
|
|
60
|
-
_rolling_buffer_size: int = 0
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _expand(value: str) -> list[bytes]:
|
|
64
|
-
"""Return the encoded forms of a token value we treat as equivalent.
|
|
65
|
-
|
|
66
|
-
Each form is added to the redaction set so ``print(base64.b64encode(token))``
|
|
67
|
-
and the cleartext ``print(token)`` are both scrubbed.
|
|
68
|
-
"""
|
|
69
|
-
if not value:
|
|
70
|
-
return []
|
|
71
|
-
raw = value.encode("utf-8")
|
|
72
|
-
forms: list[bytes] = []
|
|
73
|
-
|
|
74
|
-
def maybe_add(candidate: bytes) -> None:
|
|
75
|
-
if len(candidate) >= MIN_TOKEN_LEN:
|
|
76
|
-
forms.append(candidate)
|
|
77
|
-
|
|
78
|
-
maybe_add(raw)
|
|
79
|
-
maybe_add(base64.b64encode(raw).rstrip(b"="))
|
|
80
|
-
maybe_add(base64.b64encode(raw))
|
|
81
|
-
maybe_add(base64.urlsafe_b64encode(raw).rstrip(b"="))
|
|
82
|
-
maybe_add(base64.urlsafe_b64encode(raw))
|
|
83
|
-
maybe_add(urllib.parse.quote(value, safe="").encode("ascii"))
|
|
84
|
-
maybe_add(binascii.hexlify(raw))
|
|
85
|
-
return forms
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def register_token(value: str) -> None:
|
|
89
|
-
"""Add a token (and its derived encodings) to the redaction set.
|
|
90
|
-
|
|
91
|
-
Safe to call multiple times with the same value. No-op for empty values.
|
|
92
|
-
"""
|
|
93
|
-
global _rolling_buffer_size
|
|
94
|
-
for form in _expand(value):
|
|
95
|
-
_redactions.add(form)
|
|
96
|
-
required = 2 * len(form)
|
|
97
|
-
if required > _rolling_buffer_size:
|
|
98
|
-
_rolling_buffer_size = required
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def register_tokens(values: Iterable[str]) -> None:
|
|
102
|
-
for v in values:
|
|
103
|
-
register_token(v)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def clear_tokens() -> None:
|
|
107
|
-
"""Drop every registered token. Called at session teardown by the container."""
|
|
108
|
-
global _rolling_buffer_size
|
|
109
|
-
_redactions.clear()
|
|
110
|
-
_rolling_buffer_size = 0
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# ---------------------------------------------------------------------------
|
|
114
|
-
# Stdio wrapper
|
|
115
|
-
# ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _redact(chunk: bytes, tail_buffer: bytearray) -> tuple[bytes, bytearray]:
|
|
119
|
-
"""Return (scrubbed_output_chunk, new_tail_buffer).
|
|
120
|
-
|
|
121
|
-
The tail buffer holds the trailing N bytes of the previous write so we can
|
|
122
|
-
catch tokens split across write boundaries. The output we emit is the part
|
|
123
|
-
of ``tail_buffer + chunk`` that is safely past the trailing window; the
|
|
124
|
-
suffix stays in the new tail for the next call.
|
|
125
|
-
"""
|
|
126
|
-
if not _redactions:
|
|
127
|
-
return chunk, tail_buffer
|
|
128
|
-
|
|
129
|
-
combined = bytes(tail_buffer) + chunk
|
|
130
|
-
for form in _redactions:
|
|
131
|
-
combined = combined.replace(form, _REDACTION)
|
|
132
|
-
|
|
133
|
-
if _rolling_buffer_size and len(combined) > _rolling_buffer_size:
|
|
134
|
-
emit = combined[:-_rolling_buffer_size]
|
|
135
|
-
new_tail = bytearray(combined[-_rolling_buffer_size:])
|
|
136
|
-
else:
|
|
137
|
-
emit = b""
|
|
138
|
-
new_tail = bytearray(combined)
|
|
139
|
-
|
|
140
|
-
return emit, new_tail
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
class _RedactingStream:
|
|
144
|
-
"""Wraps sys.stdout / sys.stderr's underlying buffer with a redactor.
|
|
145
|
-
|
|
146
|
-
Intercepts both ``write(str)`` (TextIOBase) and ``.buffer.write(bytes)``
|
|
147
|
-
paths so ``print(...)``, ``sys.stdout.write(...)``, and
|
|
148
|
-
``sys.stdout.buffer.write(...)`` all flow through the scrubber.
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
def __init__(self, wrapped: Any, name: str) -> None:
|
|
152
|
-
self._wrapped = wrapped
|
|
153
|
-
self._name = name
|
|
154
|
-
self._tail = bytearray()
|
|
155
|
-
# AgentCore writes to stdout/stderr line-buffered; we flush our tail
|
|
156
|
-
# on every newline-terminated write so output appears promptly.
|
|
157
|
-
|
|
158
|
-
def write(self, data: Any) -> int:
|
|
159
|
-
if isinstance(data, str):
|
|
160
|
-
payload = data.encode("utf-8", errors="replace")
|
|
161
|
-
wrote_str = True
|
|
162
|
-
elif isinstance(data, (bytes, bytearray, memoryview)):
|
|
163
|
-
payload = bytes(data)
|
|
164
|
-
wrote_str = False
|
|
165
|
-
else:
|
|
166
|
-
# Defer unknown types to the wrapped object — let it raise.
|
|
167
|
-
return self._wrapped.write(data)
|
|
168
|
-
|
|
169
|
-
emit, self._tail = _redact(payload, self._tail)
|
|
170
|
-
# A newline forces a tail flush so output doesn't lag arbitrarily when
|
|
171
|
-
# no new writes arrive. This is a UX concession, not a security one —
|
|
172
|
-
# redaction is complete whether we flush or not.
|
|
173
|
-
if b"\n" in payload:
|
|
174
|
-
emit = emit + bytes(self._tail)
|
|
175
|
-
self._tail = bytearray()
|
|
176
|
-
|
|
177
|
-
if not emit:
|
|
178
|
-
return len(data) if wrote_str else len(payload)
|
|
179
|
-
|
|
180
|
-
if wrote_str:
|
|
181
|
-
self._wrapped.write(emit.decode("utf-8", errors="replace"))
|
|
182
|
-
else:
|
|
183
|
-
# TextIOWrapper.write raises on bytes; prefer the underlying buffer.
|
|
184
|
-
buf = getattr(self._wrapped, "buffer", None)
|
|
185
|
-
if buf is not None:
|
|
186
|
-
buf.write(emit)
|
|
187
|
-
else:
|
|
188
|
-
self._wrapped.write(emit)
|
|
189
|
-
return len(data) if wrote_str else len(payload)
|
|
190
|
-
|
|
191
|
-
def flush(self) -> None:
|
|
192
|
-
if self._tail:
|
|
193
|
-
data = bytes(self._tail)
|
|
194
|
-
self._tail = bytearray()
|
|
195
|
-
buf = getattr(self._wrapped, "buffer", None)
|
|
196
|
-
if buf is not None:
|
|
197
|
-
buf.write(data)
|
|
198
|
-
else:
|
|
199
|
-
self._wrapped.write(data.decode("utf-8", errors="replace"))
|
|
200
|
-
self._wrapped.flush()
|
|
201
|
-
|
|
202
|
-
# Forward any other attribute access (fileno, isatty, encoding, etc.) so
|
|
203
|
-
# libraries that introspect the stream still see what they expect.
|
|
204
|
-
def __getattr__(self, item: str) -> Any:
|
|
205
|
-
return getattr(self._wrapped, item)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
_installed = False
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def installed() -> bool:
|
|
212
|
-
return _installed
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _install() -> None:
|
|
216
|
-
global _installed
|
|
217
|
-
if _installed:
|
|
218
|
-
return
|
|
219
|
-
sys.stdout = _RedactingStream(sys.stdout, "stdout")
|
|
220
|
-
sys.stderr = _RedactingStream(sys.stderr, "stderr")
|
|
221
|
-
_installed = True
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
# Install unconditionally on import. Disable only for a pytest diagnostic
|
|
225
|
-
# marker the test suite can opt out of (set THINKWORK_SANDBOX_SCRUBBER=off).
|
|
226
|
-
if os.environ.get("THINKWORK_SANDBOX_SCRUBBER", "on") != "off":
|
|
227
|
-
_install()
|