Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/skills/env-reference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ See `apps/api/.env.example` for the full list. Key variables:
- `ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES` — Maximum cached ACP activity bindings retained by one Worker isolate (default: `2048`)

- `ACP_INTERACTIONS_ENABLED` — Dormant durable ACP interaction foundation kill switch. Slice A defaults this to `false`; later slices must intentionally enable producers/consumers (default: `false`)
- `ACP_INTERACTION_PERMISSION_CONVERSATION_DEADLINE_MS` / `ACP_INTERACTION_PERMISSION_TASK_DEADLINE_MS` — Default permission deadlines for conversation and task contexts (defaults: `7200000` / `1800000`)
- `ACP_INTERACTION_MAX_DEADLINE_MS` / `ACP_INTERACTION_DEADLINE_MARGIN_MS` — Absolute deadline ceiling and prompt/runtime cap safety margin (defaults: `14400000` / `60000`)
- `ACP_INTERACTION_MAX_DEADLINE_MS` — Absolute deadline ceiling for runtime-created requests (default: `14400000`)
- `ACP_INTERACTION_MAX_PENDING_PER_SESSION` — Maximum pending durable ACP interactions per chat (default: `8`)
- `ACP_INTERACTION_REQUEST_MAX_BYTES`, `ACP_INTERACTION_OPTIONS_MAX_COUNT`, `ACP_INTERACTION_OPTION_NAME_MAX_CHARS`, `ACP_INTERACTION_FORM_SCHEMA_MAX_BYTES`, `ACP_INTERACTION_FORM_SCHEMA_MAX_PROPERTIES`, `ACP_INTERACTION_FORM_SCHEMA_MAX_ENUM` — Request/detail and schema bounds for encrypted ACP interaction payloads (defaults: `32768`, `16`, `200`, `16384`, `20`, `50`)
- `ACP_INTERACTION_ANSWER_MAX_BYTES` / `ACP_INTERACTION_ANSWER_STRING_MAX_BYTES` — Answer decision and individual answer string bounds before encrypted storage (defaults: `16384` / `4096`)
- `ACP_INTERACTION_RETRY_DELAYS_MS` / `ACP_INTERACTION_RETRY_STEADY_MS` / `ACP_INTERACTION_DELIVERY_WINDOW_MS` — Durable answer outbox retry sequence, steady retry delay, and max post-answer retry window (defaults: `1000,5000,30000,120000,300000`, `300000`, `900000`)
- `ACP_INTERACTION_SENSITIVE_PURGE_MS`, `ACP_INTERACTION_SUMMARY_RETENTION_MS`, `ACP_INTERACTION_SUMMARY_LAST_SETTLED`, `ACP_INTERACTION_SNAPSHOT_LAST_SETTLED` — Sensitive encrypted payload purge, settled summary retention, and bounded snapshot controls (defaults: `3600000`, `2592000000`, `100`, `20`)
- `ACP_INTERACTION_EXPIRY_BATCH_SIZE`, `ACP_INTERACTION_OUTBOX_BATCH_SIZE`, `ACP_INTERACTION_DELIVERY_BATCH_SIZE` — Per-alarm limits for deadline settlement, due outbox selection, and no-wake VM delivery attempts (defaults: `25`, `25`, `1`)
- `ACP_INTERACTION_ALARM_WALL_TIME_MS` / `ACP_INTERACTION_ALARM_REARM_DELAY_MS` — Total alarm work budget and minimum re-arm delay (defaults: `15000` / `1000`)

Activity coalescing and binding caches are per Worker isolate. Delayed flushes carry their original observed event time, and ProjectData rejects stale writes so a delayed intermediate report cannot overwrite a newer idle/error state from another isolate.

Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/deploy-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,29 @@ jobs:
ACP_ACTIVITY_COALESCE_MAX_PENDING: ${{ vars.ACP_ACTIVITY_COALESCE_MAX_PENDING }}
ACP_ACTIVITY_BINDING_CACHE_TTL_MS: ${{ vars.ACP_ACTIVITY_BINDING_CACHE_TTL_MS }}
ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES: ${{ vars.ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES }}
ACP_INTERACTIONS_ENABLED: ${{ vars.ACP_INTERACTIONS_ENABLED }}
ACP_INTERACTION_MAX_DEADLINE_MS: ${{ vars.ACP_INTERACTION_MAX_DEADLINE_MS }}
ACP_INTERACTION_MAX_PENDING_PER_SESSION: ${{ vars.ACP_INTERACTION_MAX_PENDING_PER_SESSION }}
ACP_INTERACTION_REQUEST_MAX_BYTES: ${{ vars.ACP_INTERACTION_REQUEST_MAX_BYTES }}
ACP_INTERACTION_OPTIONS_MAX_COUNT: ${{ vars.ACP_INTERACTION_OPTIONS_MAX_COUNT }}
ACP_INTERACTION_OPTION_NAME_MAX_CHARS: ${{ vars.ACP_INTERACTION_OPTION_NAME_MAX_CHARS }}
ACP_INTERACTION_FORM_SCHEMA_MAX_BYTES: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_BYTES }}
ACP_INTERACTION_FORM_SCHEMA_MAX_PROPERTIES: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_PROPERTIES }}
ACP_INTERACTION_FORM_SCHEMA_MAX_ENUM: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_ENUM }}
ACP_INTERACTION_ANSWER_MAX_BYTES: ${{ vars.ACP_INTERACTION_ANSWER_MAX_BYTES }}
ACP_INTERACTION_ANSWER_STRING_MAX_BYTES: ${{ vars.ACP_INTERACTION_ANSWER_STRING_MAX_BYTES }}
ACP_INTERACTION_RETRY_DELAYS_MS: ${{ vars.ACP_INTERACTION_RETRY_DELAYS_MS }}
ACP_INTERACTION_RETRY_STEADY_MS: ${{ vars.ACP_INTERACTION_RETRY_STEADY_MS }}
ACP_INTERACTION_DELIVERY_WINDOW_MS: ${{ vars.ACP_INTERACTION_DELIVERY_WINDOW_MS }}
ACP_INTERACTION_SENSITIVE_PURGE_MS: ${{ vars.ACP_INTERACTION_SENSITIVE_PURGE_MS }}
ACP_INTERACTION_SUMMARY_RETENTION_MS: ${{ vars.ACP_INTERACTION_SUMMARY_RETENTION_MS }}
ACP_INTERACTION_SUMMARY_LAST_SETTLED: ${{ vars.ACP_INTERACTION_SUMMARY_LAST_SETTLED }}
ACP_INTERACTION_SNAPSHOT_LAST_SETTLED: ${{ vars.ACP_INTERACTION_SNAPSHOT_LAST_SETTLED }}
ACP_INTERACTION_EXPIRY_BATCH_SIZE: ${{ vars.ACP_INTERACTION_EXPIRY_BATCH_SIZE }}
ACP_INTERACTION_OUTBOX_BATCH_SIZE: ${{ vars.ACP_INTERACTION_OUTBOX_BATCH_SIZE }}
ACP_INTERACTION_DELIVERY_BATCH_SIZE: ${{ vars.ACP_INTERACTION_DELIVERY_BATCH_SIZE }}
ACP_INTERACTION_ALARM_WALL_TIME_MS: ${{ vars.ACP_INTERACTION_ALARM_WALL_TIME_MS }}
ACP_INTERACTION_ALARM_REARM_DELAY_MS: ${{ vars.ACP_INTERACTION_ALARM_REARM_DELAY_MS }}
SESSION_SNAPSHOT_RECOVERY_CLAIM_LEASE_MS: ${{ vars.SESSION_SNAPSHOT_RECOVERY_CLAIM_LEASE_MS }}
SESSION_LIFECYCLE_ERROR_MAX_LENGTH: ${{ vars.SESSION_LIFECYCLE_ERROR_MAX_LENGTH }}
LIBRARY_PROJECT_DELETE_CLEANUP_BATCH_SIZE: ${{ vars.LIBRARY_PROJECT_DELETE_CLEANUP_BATCH_SIZE }}
Expand Down Expand Up @@ -1183,6 +1206,29 @@ jobs:
ACP_ACTIVITY_COALESCE_MAX_PENDING: ${{ vars.ACP_ACTIVITY_COALESCE_MAX_PENDING }}
ACP_ACTIVITY_BINDING_CACHE_TTL_MS: ${{ vars.ACP_ACTIVITY_BINDING_CACHE_TTL_MS }}
ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES: ${{ vars.ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES }}
ACP_INTERACTIONS_ENABLED: ${{ vars.ACP_INTERACTIONS_ENABLED }}
ACP_INTERACTION_MAX_DEADLINE_MS: ${{ vars.ACP_INTERACTION_MAX_DEADLINE_MS }}
ACP_INTERACTION_MAX_PENDING_PER_SESSION: ${{ vars.ACP_INTERACTION_MAX_PENDING_PER_SESSION }}
ACP_INTERACTION_REQUEST_MAX_BYTES: ${{ vars.ACP_INTERACTION_REQUEST_MAX_BYTES }}
ACP_INTERACTION_OPTIONS_MAX_COUNT: ${{ vars.ACP_INTERACTION_OPTIONS_MAX_COUNT }}
ACP_INTERACTION_OPTION_NAME_MAX_CHARS: ${{ vars.ACP_INTERACTION_OPTION_NAME_MAX_CHARS }}
ACP_INTERACTION_FORM_SCHEMA_MAX_BYTES: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_BYTES }}
ACP_INTERACTION_FORM_SCHEMA_MAX_PROPERTIES: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_PROPERTIES }}
ACP_INTERACTION_FORM_SCHEMA_MAX_ENUM: ${{ vars.ACP_INTERACTION_FORM_SCHEMA_MAX_ENUM }}
ACP_INTERACTION_ANSWER_MAX_BYTES: ${{ vars.ACP_INTERACTION_ANSWER_MAX_BYTES }}
ACP_INTERACTION_ANSWER_STRING_MAX_BYTES: ${{ vars.ACP_INTERACTION_ANSWER_STRING_MAX_BYTES }}
ACP_INTERACTION_RETRY_DELAYS_MS: ${{ vars.ACP_INTERACTION_RETRY_DELAYS_MS }}
ACP_INTERACTION_RETRY_STEADY_MS: ${{ vars.ACP_INTERACTION_RETRY_STEADY_MS }}
ACP_INTERACTION_DELIVERY_WINDOW_MS: ${{ vars.ACP_INTERACTION_DELIVERY_WINDOW_MS }}
ACP_INTERACTION_SENSITIVE_PURGE_MS: ${{ vars.ACP_INTERACTION_SENSITIVE_PURGE_MS }}
ACP_INTERACTION_SUMMARY_RETENTION_MS: ${{ vars.ACP_INTERACTION_SUMMARY_RETENTION_MS }}
ACP_INTERACTION_SUMMARY_LAST_SETTLED: ${{ vars.ACP_INTERACTION_SUMMARY_LAST_SETTLED }}
ACP_INTERACTION_SNAPSHOT_LAST_SETTLED: ${{ vars.ACP_INTERACTION_SNAPSHOT_LAST_SETTLED }}
ACP_INTERACTION_EXPIRY_BATCH_SIZE: ${{ vars.ACP_INTERACTION_EXPIRY_BATCH_SIZE }}
ACP_INTERACTION_OUTBOX_BATCH_SIZE: ${{ vars.ACP_INTERACTION_OUTBOX_BATCH_SIZE }}
ACP_INTERACTION_DELIVERY_BATCH_SIZE: ${{ vars.ACP_INTERACTION_DELIVERY_BATCH_SIZE }}
ACP_INTERACTION_ALARM_WALL_TIME_MS: ${{ vars.ACP_INTERACTION_ALARM_WALL_TIME_MS }}
ACP_INTERACTION_ALARM_REARM_DELAY_MS: ${{ vars.ACP_INTERACTION_ALARM_REARM_DELAY_MS }}
SESSION_SNAPSHOT_RECOVERY_CLAIM_LEASE_MS: ${{ vars.SESSION_SNAPSHOT_RECOVERY_CLAIM_LEASE_MS }}
SESSION_LIFECYCLE_ERROR_MAX_LENGTH: ${{ vars.SESSION_LIFECYCLE_ERROR_MAX_LENGTH }}
LIBRARY_PROJECT_DELETE_CLEANUP_BATCH_SIZE: ${{ vars.LIBRARY_PROJECT_DELETE_CLEANUP_BATCH_SIZE }}
Expand Down
8 changes: 5 additions & 3 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -797,10 +797,7 @@ INFOMANIAK_IP_POLL_INTERVAL_MS=3000
# ACP_ACTIVITY_BINDING_CACHE_MAX_ENTRIES=2048 # Max cached ACP activity bindings per Worker isolate
# Dormant durable ACP interaction foundation. Slice A keeps this disabled; later slices wire runtime/UI producers and consumers.
# ACP_INTERACTIONS_ENABLED=false # Fail-closed feature flag for durable ACP interaction creation
# ACP_INTERACTION_PERMISSION_CONVERSATION_DEADLINE_MS=7200000 # Default conversation permission deadline (2h)
# ACP_INTERACTION_PERMISSION_TASK_DEADLINE_MS=1800000 # Default task permission deadline (30m)
# ACP_INTERACTION_MAX_DEADLINE_MS=14400000 # Absolute max request deadline (4h)
# ACP_INTERACTION_DEADLINE_MARGIN_MS=60000 # Safety margin before prompt/runtime cap
# ACP_INTERACTION_MAX_PENDING_PER_SESSION=8 # Pending durable interactions allowed per chat
# ACP_INTERACTION_REQUEST_MAX_BYTES=32768 # Encrypted request detail cap
# ACP_INTERACTION_OPTIONS_MAX_COUNT=16 # Permission option count cap
Expand All @@ -817,6 +814,11 @@ INFOMANIAK_IP_POLL_INTERVAL_MS=3000
# ACP_INTERACTION_SUMMARY_RETENTION_MS=2592000000 # Retain bounded settled summaries for 30d
# ACP_INTERACTION_SUMMARY_LAST_SETTLED=100 # Min settled summaries preserved during retention compaction
# ACP_INTERACTION_SNAPSHOT_LAST_SETTLED=20 # Settled summaries returned per snapshot page
# ACP_INTERACTION_EXPIRY_BATCH_SIZE=25 # Pending deadlines settled per alarm pass
# ACP_INTERACTION_OUTBOX_BATCH_SIZE=25 # Due outbox rows considered per alarm pass
# ACP_INTERACTION_DELIVERY_BATCH_SIZE=1 # VM answer deliveries attempted per alarm pass
# ACP_INTERACTION_ALARM_WALL_TIME_MS=15000 # Total InteractionStore alarm work budget
# ACP_INTERACTION_ALARM_REARM_DELAY_MS=1000 # Minimum delay before re-arming due work
# CREDENTIAL_LIMIT_WARNING_PERCENT=75 # Advisory provider credential quota warning threshold
# CREDENTIAL_LIMIT_CRITICAL_PERCENT=90 # Advisory provider credential quota critical threshold

Expand Down
42 changes: 36 additions & 6 deletions apps/api/src/durable-objects/interaction-store-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export type InteractionStoreAnswerResult =
| {
status: 'answered' | 'already_answered';
summary: AcpInteractionSafeSummary;
delivery: { generation: string; runtimeIdentity: string };
delivery: { generation: string; runtimeIdentity: string; agentSessionId: string };
}
| {
status: 'not_found' | 'stale' | 'conflict' | 'answer_key_conflict' | 'payload_too_large';
Expand All @@ -97,6 +97,12 @@ export async function sha256(value: string): Promise<string> {
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
}

export async function interactionDecisionHash(
decision: AcpInteractionAnswerDecision
): Promise<string> {
return sha256(canonicalJson(decision));
}

export function parseSummary(row: InteractionRow): AcpInteractionSafeSummary {
const safeSummary = JSON.parse(row.safe_summary_json) as unknown;
return {
Expand Down Expand Up @@ -136,6 +142,15 @@ export function detailBoundsViolation(
return detailRecordBoundsViolation(asRecord(detail), config);
}

export function answerBoundsViolation(
decision: AcpInteractionAnswerDecision,
config: AcpInteractionConfig
): string | null {
return hasOversizedString(decision, config.answerStringMaxBytes)
? 'answer string exceeds configured maximum'
: null;
}

function detailRecordBoundsViolation(
record: Record<string, unknown> | null,
config: AcpInteractionConfig
Expand All @@ -159,9 +174,26 @@ function optionsBoundsViolation(
if (Array.isArray(record.options) && record.options.length > config.optionsMaxCount) {
return 'request options exceed configured maximum';
}
if (
Array.isArray(record.options) &&
record.options.some((option) => {
const optionRecord = asRecord(option);
const name = optionRecord?.name ?? optionRecord?.label;
return typeof name === 'string' && [...name].length > config.optionNameMaxChars;
})
) {
return 'request option name exceeds configured maximum';
}
return null;
}

function hasOversizedString(value: unknown, maxBytes: number): boolean {
if (typeof value === 'string') return new TextEncoder().encode(value).byteLength > maxBytes;
if (Array.isArray(value)) return value.some((item) => hasOversizedString(item, maxBytes));
const record = asRecord(value);
return record ? Object.values(record).some((item) => hasOversizedString(item, maxBytes)) : false;
}

function schemaBoundsViolation(
record: Record<string, unknown>,
config: AcpInteractionConfig
Expand All @@ -175,17 +207,15 @@ function schemaBoundsViolation(
return schemaObjectBoundsViolation(schema, config);
}

function schemaObjectBoundsViolation(
schema: unknown,
config: AcpInteractionConfig
): string | null {
function schemaObjectBoundsViolation(schema: unknown, config: AcpInteractionConfig): string | null {
const schemaRecord = asRecord(schema);
if (!schemaRecord) return null;
const properties = asRecord(schemaRecord.properties);
if (properties && Object.keys(properties).length > config.formSchemaMaxProperties) {
return 'form schema properties exceed configured maximum';
}
if (hasEnumOverflow(schema, config.formSchemaMaxEnum)) return 'form schema enum exceeds configured maximum';
if (hasEnumOverflow(schema, config.formSchemaMaxEnum))
return 'form schema enum exceeds configured maximum';
return null;
}

Expand Down
Loading
Loading