Skip to content

Commit e4bb128

Browse files
committed
Merge remote-tracking branch 'origin/staging' into codex/enterprise-reporting-period
# Conflicts: # packages/db/migrations/meta/0292_snapshot.json # packages/db/migrations/meta/_journal.json # scripts/check-api-validation-contracts.ts
2 parents bd43b1e + 10ff622 commit e4bb128

91 files changed

Lines changed: 24488 additions & 2076 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/en/logs-debugging/logging.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ import { FAQ } from '@/components/ui/faq'
9494
<FAQ items={[
9595
{ question: "How long are run logs retained?", answer: "Free plans retain logs for 7 days — after that, logs are archived to cloud storage and deleted from the database. Pro, Team, and Enterprise plans retain logs indefinitely with no automatic cleanup." },
9696
{ question: "What data is captured in each run log?", answer: "Each log entry includes the run ID, workflow ID, trigger type, start and end timestamps, total duration in milliseconds, cost breakdown (total cost, token counts, and per-model breakdowns), run data with trace spans, final output, and any associated files. The log details sidebar lets you inspect block-level inputs and outputs." },
97-
{ question: "Are saved secrets visible in logs?", answer: "When a value saved under Secrets is successfully substituted through {{KEY}}, exact, case-sensitive occurrences are masked throughout the log-facing copy, including the live block-log display, Logs Overview input and output, Trace, log-read APIs, and the Logs block's Get Run Details output. This is not a general redactor: hardcoded or directly read values do not activate masking by themselves, and encoded, hashed, or transformed values are not matched. Functional execution responses, streams, and callbacks remain unchanged. See Execution log protection under Secrets for details." },
97+
{ question: "Are saved secrets visible in logs?", answer: "When a value saved under Secrets is successfully substituted through {{KEY}}, exact, case-sensitive occurrences are masked throughout the log-facing copy, including the live block-log display, Logs Overview input and output, Trace, log-read APIs, and the Logs block's Get Run Details output. Direct reads such as environmentVariables['KEY'] or shell $KEY also activate masking when Sim can recognize the read in the code beforehand; a name built at runtime, a reassigned binding, or a hardcoded literal is not recognized. This is not a general redactor: encoded, hashed, or transformed values are not matched. Functional execution responses, streams, and callbacks remain unchanged. See Execution log protection under Secrets for details." },
9898
{ question: "What is a workflow snapshot?", answer: "A frozen copy of the workflow's structure (blocks, connections, and configuration) captured at run time, so you can see the exact state behind a particular run — useful for debugging workflows that have been modified since." },
9999
{ question: "Can I access logs programmatically?", answer: "Yes. The External API provides endpoints to query logs with filtering by workflow, time range, trigger type, duration, cost, and model. You can also set up webhook, email, or Slack notifications for real-time alerts when runs complete." },
100100
{ question: "What does Live mode do on the Logs page?", answer: "It refreshes the Logs page in real time so new entries appear as they are recorded — useful during deployments or when monitoring active workflows." },

apps/docs/content/docs/en/platform/credentials.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,22 @@ When a saved secret is successfully substituted through a `{{KEY}}` reference, S
7171

7272
Secret resolution and functional workflow behavior are unchanged: blocks, tools, and downstream steps receive the real runtime value. Stored functional execution data, workflow execution responses, streams, callbacks, block state, and snapshots are not rewritten. Log-facing views and read APIs receive a separate protected copy, so the Logs Overview **Workflow Input** and **Workflow Output** are masked without changing the underlying workflow result. Model requests receive another protected projection: exact secret values known to the run are replaced with `{{KEY}}` before model-visible messages, prompts, tool arguments, or tool continuations leave Sim.
7373

74+
Code that reads a secret straight off the runtime environment — `environmentVariables['KEY']`, `environmentVariables.KEY`, or `const { KEY } = environmentVariables` in JavaScript, `environmentVariables['KEY']` or `environmentVariables.get('KEY')` in Python, `$KEY` or `${KEY}` in shell — also activates masking, provided Sim can see the read in the code before it runs. A hardcoded literal never does: Sim has no way to know it came from a secret.
75+
7476
<Callout type="warn">
75-
Execution-log masking is activated only when Sim successfully resolves a value from **Settings → Secrets** through `{{KEY}}`. A hardcoded literal, direct `environmentVariables['KEY']` read, or shell `$KEY` read does not activate log masking by itself. Model-bound projection also checks the run's authorized secret catalog, including direct reads, but both protections match only exact values. Encoded, hashed, fragmented, or otherwise transformed versions are not matched. Do not deliberately return or print secrets.
77+
Direct reads are found by reading the code, not by running it, so recognition stops where the code stops being readable ahead of time. An unrecognized read is not masked, and does not appear under **See usage**.
78+
79+
Where a read is recognized, Sim reports it rather than trying to prove it is not one. Code that shadows the environment binding with its own object, overwrites a variable before reading it, or assigns to the name instead of reading it is still reported. Naming a secret costs only an exact value the code never emits; failing to name one leaves it unmasked. **See usage** can therefore occasionally list a secret the code had available but did not read.
80+
81+
Assigning to the injected binding does not change the stored secret — it is an ordinary object built from the run's payload and discarded when the run ends. Edit a secret under **Settings → Secrets**.
82+
83+
A read is **not** recognized when:
84+
85+
- **The name is built at runtime.** `environmentVariables[keyName]`, `$@`, `${!indirect}`, `eval`, `printenv`, or a sourced file hide which secret is being read.
86+
- **The read is of a different object.** `other.environmentVariables['KEY']` reads something that merely shares the name.
87+
- **The read cannot be told apart from text.** A `$KEY` inside single quotes or a quoted heredoc (`<<'EOF'`) never expands, and Sim treats anything its scanner cannot place as not running.
88+
89+
Both masking and model-bound projection match only exact values in either case. Encoded, hashed, fragmented, or otherwise transformed versions are not matched, and a value assembled or emitted piece by piece cannot be matched at all — determining whether arbitrary code will eventually reveal a value is not decidable in general. Treat these as a safety net, not a boundary: do not deliberately return, print, or transmit secrets.
7690
</Callout>
7791

7892
### Copilot code execution
@@ -105,9 +119,22 @@ From here you can:
105119
- View the **Key** and edit the **Value**
106120
- Edit the **Description** — an optional note telling teammates what the secret is for. Workspace secrets only; a personal secret is not shared, so it has none
107121
- Manage **Members** — invite teammates by email and assign them an **Admin** or **Member** role
122+
- Open **See usage** — where this secret has actually been used
108123

109124
Click **Save** to apply changes, or **Back** to return to the list.
110125

126+
### See usage
127+
128+
**See usage** lists the runs that resolved this secret: when it was last used, what used it (a workflow, the Sim agent, or an MCP server), how it was triggered, who it resolved under, and a link to the most recent run in Logs. Rows are grouped by day, so a workflow on a schedule reads as one row per day rather than thousands.
129+
130+
This answers the question worth asking before rotating a key: who has been using it, inside what, and how recently.
131+
132+
Only people who can read the value can see it — a Credential Admin on a workspace secret, or the owner of a personal one. For everyone else the action is visible but disabled, because the trail names workflows, people, and run IDs, which is the same information masking withholds. Two people who each hold a personal secret under the same name see only their own runs.
133+
134+
<Callout>
135+
Usage is recorded independently of execution logs, so it outlives them: logs expire under your workspace's retention setting, while the record of who touched a credential does not. It records what a run resolved, subject to the recognition limits under [Execution log protection](#execution-log-protection) — a read Sim cannot attribute is left out rather than guessed at, so treat an empty trail as "nothing recognized," not proof a secret was never used.
136+
</Callout>
137+
111138
## Workspace vs. Personal
112139

113140
| | Workspace | Personal |

apps/docs/content/docs/en/workflows/blocks/function.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,13 @@ packages, and 10 managed CLI tools.
279279

280280
When a Function block is used as an Agent tool, its code can read every workspace
281281
secret by default — both `{{MY_SECRET}}` and `environmentVariables['MY_SECRET']`.
282-
Use `{{MY_SECRET}}` when the value may appear in execution logs: a successful
283-
double-brace substitution activates [execution-trace masking](/platform/credentials#execution-log-protection),
284-
while direct `environmentVariables['MY_SECRET']` access alone does not activate
285-
it by itself.
282+
Prefer `{{MY_SECRET}}` when the value may appear in execution logs. A successful
283+
double-brace substitution always activates
284+
[execution-trace masking](/platform/credentials#execution-log-protection). A direct
285+
`environmentVariables['MY_SECRET']` read activates it too, but only when Sim can
286+
recognize the read in the code beforehand — a name built at runtime, or a file that
287+
reassigns `environmentVariables` itself, is not recognized. See
288+
[the recognition limits](/platform/credentials#execution-log-protection).
286289

287290
To narrow that, set **Secret access** to *Selected secrets* in the block's
288291
tool configuration and pick the names the code may read. Two things change:

apps/sim/AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,13 @@ export function useEntityList(workspaceId?: string) {
243243
- **Create `utils.ts` when** 2+ files need the same helper
244244
- **Check existing sources** before duplicating (`lib/` has many utilities)
245245
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)
246+
247+
<!-- BEGIN:nextjs-agent-rules -->
248+
249+
# This is NOT the Next.js you know
250+
251+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
252+
253+
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
254+
255+
<!-- END:nextjs-agent-rules -->

apps/sim/app/_shell/consent/consent-preferences.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,29 @@ const CONSENT_CATEGORY_COPY: Record<string, ConsentCategoryCopy | undefined> = {
4242
},
4343
} satisfies Record<ConsentCategory, ConsentCategoryCopy>
4444

45+
/** The runtime's category union, without re-declaring it. */
46+
type ConsentCategoryName = Parameters<ReturnType<typeof useConsentManager>['setSelectedConsent']>[0]
47+
48+
interface ConsentPreferencesProps {
49+
/**
50+
* Called after a switch stages its new value, for a surface that commits per
51+
* toggle. `revert` puts the category back, for a commit that then fails. The
52+
* banner omits this and commits from its own footer instead.
53+
*/
54+
onChange?: (change: { name: ConsentCategoryName; revert: () => void }) => void
55+
/** Locks every switch, e.g. while a commit is in flight. */
56+
disabled?: boolean
57+
}
58+
4559
/**
4660
* The per-category consent switches, shared by the two surfaces that offer
4761
* them: the banner's expanded state and the Privacy settings page. Both write
48-
* to `selectedConsents`; committing is the caller's, since the banner saves
49-
* from its own footer and settings saves from the shell's header.
62+
* to `selectedConsents`; whether that is then committed is the caller's, via
63+
* {@link ConsentPreferencesProps.onChange}.
5064
*
51-
* Must be rendered inside a `ConsentManagerProvider`.
65+
* Must be rendered inside a `ConsentStoreProvider`.
5266
*/
53-
export function ConsentPreferences() {
67+
export function ConsentPreferences({ onChange, disabled = false }: ConsentPreferencesProps) {
5468
const { consents, selectedConsents, setSelectedConsent, getDisplayedConsents } =
5569
useConsentManager()
5670

@@ -77,8 +91,14 @@ export function ConsentPreferences() {
7791
<Switch
7892
id={inputId}
7993
checked={selectedConsents[type.name] ?? consents[type.name] ?? false}
80-
disabled={type.disabled}
81-
onCheckedChange={(checked) => setSelectedConsent(type.name, checked)}
94+
disabled={type.disabled || disabled}
95+
onCheckedChange={(checked) => {
96+
setSelectedConsent(type.name, checked)
97+
onChange?.({
98+
name: type.name,
99+
revert: () => setSelectedConsent(type.name, !checked),
100+
})
101+
}}
82102
/>
83103
</li>
84104
)

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2441,6 +2441,50 @@ describe('Function Execute API Route', () => {
24412441
expect(sandboxRequest.privateInputs[0].content).toContain('$UNRELATED `touch /tmp/nope`')
24422442
})
24432443

2444+
/**
2445+
* The founding scenario of the usage trail: code that reads a secret and emits it only in
2446+
* transformed form. No output ever matches the value, so an output-gated report said
2447+
* "never used" for exactly the run an admin needs to see. A referenced secret reports
2448+
* whether or not its value surfaces.
2449+
*/
2450+
it('reports a secret exfiltrated character by character', async () => {
2451+
mockExecuteInIsolatedVM.mockResolvedValueOnce({
2452+
result: 's|e|c|r|e|t|-|v|a|l|u|e|-|1|2|3|4',
2453+
stdout: '',
2454+
})
2455+
const response = await POST(
2456+
createMockRequest(
2457+
'POST',
2458+
{
2459+
code: "const k = '{{API_KEY}}'; return k.split('').join('|')",
2460+
envVars: { API_KEY: 'secret-value-1234' },
2461+
},
2462+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2463+
)
2464+
)
2465+
2466+
expect(response.status).toBe(200)
2467+
expect((await response.json()).__resolvedSecretNames).toEqual(['API_KEY'])
2468+
})
2469+
2470+
/** The ordinary silent use: the key authenticates a call and never appears in output. */
2471+
it('reports a secret used without appearing in the output', async () => {
2472+
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: { status: 200 }, stdout: '' })
2473+
const response = await POST(
2474+
createMockRequest(
2475+
'POST',
2476+
{
2477+
code: "await fetch('https://api.example.com', { headers: { auth: environmentVariables['API_KEY'] } }); return { status: 200 }",
2478+
envVars: { API_KEY: 'secret-value-1234' },
2479+
},
2480+
{ 'x-sim-request-private-tool-metadata': 'resolved-secret-names-v1' }
2481+
)
2482+
)
2483+
2484+
expect(response.status).toBe(200)
2485+
expect((await response.json()).__resolvedSecretNames).toEqual(['API_KEY'])
2486+
})
2487+
24442488
it('does not report a reference when validation rejects before code resolution', async () => {
24452489
const response = await POST(
24462490
createMockRequest(
@@ -2469,7 +2513,12 @@ describe('Function Execute API Route', () => {
24692513
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
24702514
})
24712515

2472-
it('reports exact secret values returned through placeholders without inferring direct environment reads', async () => {
2516+
/**
2517+
* A direct read is a factual reference to the environment binding, not the value-coincidence
2518+
* inference #6374 removed — that one claimed a secret because its plaintext happened to equal
2519+
* an unrelated output. Reporting it is what activates execution-log masking for the value.
2520+
*/
2521+
it('reports secrets reached through placeholders and through direct environment reads', async () => {
24732522
mockExecuteInIsolatedVM.mockResolvedValueOnce({
24742523
result: 'secret-valueother-secret',
24752524
stdout: '',
@@ -2507,14 +2556,14 @@ describe('Function Execute API Route', () => {
25072556

25082557
expect(envData.__resolvedSecretNames).toEqual(['ENV_ONLY', 'SHARED'])
25092558
expect(directData.output.result).toBe('secret-value')
2510-
expect(directData.__resolvedSecretNames).toEqual([])
2559+
expect(directData.__resolvedSecretNames).toEqual(['API_KEY'])
25112560
})
25122561

25132562
it.each([
25142563
{ name: 'numeric', secret: '123', result: 123 },
25152564
{ name: 'boolean', secret: 'true', result: true },
25162565
])(
2517-
'preserves a typed $name value returned through legacy direct environment access without inferred provenance',
2566+
'preserves a typed $name value returned through a direct environment read while reporting it',
25182567
async ({ secret, result }) => {
25192568
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result, stdout: '' })
25202569

@@ -2532,12 +2581,13 @@ describe('Function Execute API Route', () => {
25322581
)
25332582
const data = await response.json()
25342583

2584+
/** The typed value survives: a secret this short is never substitutable. */
25352585
expect(data.output.result).toBe(result)
2536-
expect(data.__resolvedSecretNames).toEqual([])
2586+
expect(data.__resolvedSecretNames).toEqual(['API_KEY'])
25372587
}
25382588
)
25392589

2540-
it('reports placeholder output without inferring provenance from legacy shell environment access', async () => {
2590+
it('reports placeholder output and a shell environment expansion alike', async () => {
25412591
envFlagsMock.isRemoteSandboxEnabled = true
25422592
mockExecuteShellInSandbox.mockResolvedValueOnce({
25432593
result: null,
@@ -2582,7 +2632,7 @@ describe('Function Execute API Route', () => {
25822632

25832633
expect(referencedData.__resolvedSecretNames).toEqual(['API_KEY'])
25842634
expect(directData.output.stdout).toBe('secret-value')
2585-
expect(directData.__resolvedSecretNames).toEqual([])
2635+
expect(directData.__resolvedSecretNames).toEqual(['API_KEY'])
25862636
})
25872637

25882638
it('returns nonzero shell stderr as a visible 422 error and diagnostic output', async () => {
@@ -2672,7 +2722,14 @@ describe('Function Execute API Route', () => {
26722722
expect((await response.json()).__resolvedSecretNames).toEqual(['__proto__'])
26732723
})
26742724

2675-
it('does not activate a referenced secret that does not cross the Function result', async () => {
2725+
/**
2726+
* Previously asserted the inverse: a referenced secret whose value stayed out of the
2727+
* result reported nothing. That gate made the trail miss silent use — the ordinary
2728+
* API-call case and the transformed-exfiltration case alike — so activation now follows
2729+
* the referenced set. The value never appearing costs nothing downstream; the masking
2730+
* matcher simply never fires on it.
2731+
*/
2732+
it('activates a referenced secret even when its value never crosses the result', async () => {
26762733
mockExecuteInIsolatedVM.mockResolvedValueOnce({ result: 'safe-result', stdout: '' })
26772734

26782735
const response = await POST(
@@ -2686,7 +2743,7 @@ describe('Function Execute API Route', () => {
26862743
)
26872744
)
26882745

2689-
expect((await response.json()).__resolvedSecretNames).toEqual([])
2746+
expect((await response.json()).__resolvedSecretNames).toEqual(['API_KEY'])
26902747
})
26912748

26922749
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {

0 commit comments

Comments
 (0)