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
25 changes: 25 additions & 0 deletions .changeset/20751-services-strings-stage2-state-the-decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@objectstack/connector-mcp': patch
'@objectstack/plugin-email': patch
'@objectstack/service-knowledge': patch
'@objectstack/service-queue': patch
'@objectstack/service-sms': patch
'@objectstack/service-storage': patch
'@objectstack/trigger-record-change': patch
---

MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words

Clause-②: no

Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.

- `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy.
- `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata.
- `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted.
- `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window.
- `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number.
- `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on.
- `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated.

Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.
6 changes: 4 additions & 2 deletions packages/connectors/connector-mcp/src/mcp-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,16 @@ function assertDeclarativeStdioAllowed(
throw new Error(
`connector-mcp provider: connector '${connectorName}' declares a stdio transport with command '${command}', ` +
`which is not in the host's declarativeStdio allowlist [${policy.join(', ')}]. ` +
`Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted (#3055).`,
`Add the command to new ConnectorMcpPlugin({ declarativeStdio: [...] }) if this server is trusted: a stdio ` +
`transport launches a local process, so stack metadata may only name a command the host's own code allows.`,
);
}
throw new Error(
`connector-mcp provider: connector '${connectorName}' declares a stdio transport (command '${command}'), ` +
`but declarative stdio transports are disabled by default — a stdio transport launches a local process ` +
`from stack metadata (including runtime Studio publishes). If this server is trusted, opt in deliberately: ` +
`new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) — or use an http transport (#3055, ADR-0024 §4).`,
`new ConnectorMcpPlugin({ declarativeStdio: ['${command}'] }) — or use an http transport, which this policy ` +
`does not gate (ADR-0024 §4).`,
);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/plugin-email/src/email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,8 @@ export class EmailService implements IEmailService {
`EmailService: queue delivery skipped for one message — its attachments total `
+ `${encodedAttachments.totalBytes} bytes, over the ${SYS_EMAIL_ATTACHMENT_LIMIT_BYTES}-byte limit a `
+ 'sys_email row carries, so the message was delivered inline (in-process retries only) rather than '
+ 'queued without them. Content that large is queueable through the storage capability (#5172), '
+ 'queued without them. Content that large is queueable through the storage capability, which holds '
+ 'it outside the row while the row keeps a reference and the attachment\'s audit metadata, '
+ `but ${encodedAttachments.storageDetail ?? 'that path was not attempted for this message'}. `
+ 'Fix: mount the storage capability (@objectstack/service-storage) so large attachments are '
+ 'stored out of the row and the message can be delivered durably.',
Expand Down
5 changes: 3 additions & 2 deletions packages/plugins/plugin-email/src/internal-header-readback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ export async function readInternalHeadersJson(
throw new Error(
`EmailService: ${SYS_EMAIL_OBJECT}.${HEADERS_COLUMN} is declared \`internal: true\`, but this `
+ 'data engine does not implement resolveInternalField() — the custom headers this message was '
+ 'authored with are stored but cannot be recovered, and a message must not be sent missing them '
+ '(#8149). The row stays `queued`: the queue retry or the next boot outbox sweep delivers it '
+ 'authored with are stored but cannot be recovered, and a message must not be sent missing them: '
+ 'a missing header does not announce itself, so the send would succeed while silently deviating '
+ 'from what was authored. The row stays `queued`: the queue retry or the next boot outbox sweep delivers it '
+ 'intact once an engine that implements the privileged accessor is mounted.',
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/plugins/plugin-email/src/templates/auth-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ to end other sessions.`,
variables: EMAIL_CHANGE_NOTICE_VARIABLES,
active: true,
isSystem: true,
description: 'Sent to the PREVIOUS address when a change-email request is accepted (#8019). Notification only — never gates the change.',
description: 'Sent to the PREVIOUS address when a change-email request is accepted, so a hijacked session cannot move the account identity unannounced. Notification only — never gates the change.',
};

export const AUTH_EMAIL_CHANGE_NOTICE_TEMPLATE_ZH_CN: EmailTemplate = {
Expand Down Expand Up @@ -598,7 +598,7 @@ export const AUTH_EMAIL_CHANGE_NOTICE_TEMPLATE_ZH_CN: EmailTemplate = {

如果这不是您本人的操作,您的账号可能已被入侵。请立即联系您的 {{appName}} 管理员
或支持团队,并修改密码以结束其他会话。`,
description: '在接受变更邮箱请求时发送至原邮箱地址(#8019)。仅为通知,绝不阻断变更流程。',
description: '在接受变更邮箱请求时发送至原邮箱地址,使被劫持的会话无法在原邮箱不知情的情况下转移账号身份。仅为通知,绝不阻断变更流程。',
};

export const AUTH_EMAIL_CHANGE_NOTICE_TEMPLATE_JA_JP: EmailTemplate = {
Expand Down Expand Up @@ -627,7 +627,7 @@ export const AUTH_EMAIL_CHANGE_NOTICE_TEMPLATE_JA_JP: EmailTemplate = {
心当たりがない場合、アカウントが不正利用されている可能性があります。直ちに
{{appName}} の管理者またはサポートへご連絡のうえ、パスワードを変更して他の
セッションを終了してください。`,
description: '変更メールの要求が受理された際に変更前のアドレスへ送信されます(#8019)。通知のみで、変更を妨げることはありません。',
description: '変更メールの要求が受理された際に変更前のアドレスへ送信され、乗っ取られたセッションが元のアドレスに知られないままアカウントの識別情報を移せないようにします。通知のみで、変更を妨げることはありません。',
};

export const AUTH_EMAIL_CHANGE_NOTICE_TEMPLATE_ES_ES: EmailTemplate = {
Expand Down Expand Up @@ -657,7 +657,7 @@ aprobación.
Si no has solicitado este cambio, tu cuenta podría estar comprometida. Ponte en
contacto de inmediato con el administrador o el equipo de soporte de {{appName}}
y cambia tu contraseña para cerrar las demás sesiones.`,
description: 'Se envía a la dirección ANTERIOR cuando se acepta una solicitud de cambio de correo (#8019). Solo notificación; nunca bloquea el cambio.',
description: 'Se envía a la dirección ANTERIOR cuando se acepta una solicitud de cambio de correo, para que una sesión secuestrada no pueda trasladar la identidad de la cuenta sin aviso. Solo notificación; nunca bloquea el cambio.',
};

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-i18n/src/i18n-route-ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export interface I18nRouteLedgerEntry {
export const I18N_ROUTE_LEDGER: readonly I18nRouteLedgerEntry[] = [
{ route: 'GET /api/v1/i18n/locales', family: 'i18n', disposition: 'sdk', client: 'i18n.getLocales' },
{ route: 'GET /api/v1/i18n/translations/:locale', family: 'i18n', disposition: 'sdk', client: 'i18n.getTranslations',
note: 'was a wire-level 404 — the client sent /translations?locale=xx, a shape no server mounts (the dispatcher domain body accepts it, but nothing routes a bare /translations to that body). Since #3636 the client sends the path form the spec declares.' },
note: 'was a wire-level 404 — the client sent /translations?locale=xx, a shape no server mounts (the dispatcher domain body accepts it, but nothing routes a bare /translations to that body). The client now sends the path form the spec declares.' },
{ route: 'GET /api/v1/i18n/labels/:object/:locale', family: 'i18n', disposition: 'sdk', client: 'i18n.getFieldLabels',
note: 'ditto — the client sent /labels/:object?locale=xx against a two-path-param mount (#3636).' },
note: 'ditto — the client sent /labels/:object?locale=xx against a two-path-param mount; it now sends both the object and the locale in the path.' },
];
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ describe('#4626 — KnowledgeServicePlugin event sync on data.record.*', () => {
await deliver(BULK_DELETED);

const [message] = harness.ctx.logger.warn.mock.calls.at(-1) as [string];
expect(message).toContain('#4639');
expect(message).toContain('lifecycle reap guard (#4672)');
expect(message).toContain('cannot be repaired from the event stream');
expect(message).toContain('the lifecycle reap guard de-indexes those rows before they are deleted');
expect(message).toContain('application-level predicate writes are not');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,9 @@ export class KnowledgeServicePlugin implements Plugin {
`KnowledgeServicePlugin: '${object}' had a predicate write (${type}) affecting ` +
`${typeof matched === 'number' ? matched : 'an unreported number of'} record(s). ` +
'A bulk event carries a count, not records, so the knowledge index for this object ' +
'may now be stale and cannot be repaired from the event stream (#4639). ' +
'Retention-sweep deletes are covered separately by the lifecycle reap guard (#4672); ' +
'may now be stale and cannot be repaired from the event stream. ' +
'Retention-sweep deletes are covered separately: the lifecycle reap guard de-indexes those rows ' +
'before they are deleted; ' +
'application-level predicate writes are not, and need an explicit reindexSource.',
{ object, type, matched },
);
Expand Down
3 changes: 2 additions & 1 deletion packages/services/service-knowledge/src/knowledge-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ export class KnowledgeService implements IKnowledgeService {
// No identity → fail closed on object-backed hits (keep file/http hits).
if (!ctx) {
this.options.logger?.warn?.(
'[knowledge] retrieval with no ExecutionContext — dropping object-source hits to stay safe (#2981). ' +
'[knowledge] retrieval with no ExecutionContext — dropping object-source hits to stay safe: a missing ' +
'identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped. ' +
'Pass the caller identity (or an explicit system context) to retrieve object-backed knowledge.',
);
return hits.filter((h) => !h.sourceRecordId);
Expand Down
6 changes: 4 additions & 2 deletions packages/services/service-queue/src/db-queue-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ export function completedRetentionWindowMs(): number {
if (!maxAge) {
throw new Error(
'[service-queue] sys_job_queue no longer declares lifecycle.retention — DbQueueAdapter dedups against '
+ 'terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). '
+ 'Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here.',
+ 'terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057). '
+ 'Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here: '
+ 'the one platform reaper sweeps completed rows by that declaration, and a sweeper here would be a '
+ 'second copy of the window, free to drift from the first.',
);
}
return lifecycleDurationMs(maxAge);
Expand Down
7 changes: 4 additions & 3 deletions packages/services/service-queue/src/queue-service-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,10 @@ export class QueueServicePlugin implements Plugin {
// A floor the service refused is a wiring bug in THIS plugin, not a
// degraded deployment — but it must not stop the queue from coming up.
ctx.logger.error(
'QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` '
+ 'settings override may now shorten sys_job_queue.retention below the idempotency window, in which case '
+ 'publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep '
'QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor, and that floor is '
+ 'what makes it refuse an override below the idempotency window. A `lifecycle` settings override may now '
+ 'shorten sys_job_queue.retention below that window, in which case publish would silently re-accept '
+ 'duplicates. Fix the floor registration, or keep '
+ 'lifecycle.retention_overrides.sys_job_queue unset.',
err as any,
);
Expand Down
3 changes: 2 additions & 1 deletion packages/services/service-sms/src/sms-daily-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ export class SmsDailyQuota {
'[sms] daily SMS quota counter is unreadable (' +
String((err as Error)?.message ?? err) +
') — the gate is FAILING OPEN and today\'s spend is unbounded until the counter store recovers. ' +
'Sign-in is deliberately not taken down with it (#2814).',
'Sign-in is deliberately not taken down with it: a quota the platform cannot count must not ' +
'refuse the one-time codes users sign in with.',
);
}
return { ok: true };
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-sms/src/sms-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class SmsServicePlugin implements Plugin {
resolveCache,
logger: ctx.logger,
logPrefix: '[sms]',
subject: 'daily SMS send quota (#2814)',
subject: 'daily SMS send quota',
degradedImpact:
'The ceiling is still enforced, but PER NODE: an N-node deployment can spend up to N× the ' +
'configured number of PAID SMS per day, which is exactly the total-cost hole this gate exists to close',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@ export function createSysFileReapGuard(
`file-as-reference migration is not verified, or a deviation has been observed since it ` +
`was (a value an OS_ALLOW_LAX_* escape hatch admitted against the migration's own ` +
`contract). Either way: fix the data, then run \`os migrate files-to-references --apply\`. ` +
`See sys_migration.verified_at / deviation_observed_at (ADR-0104 / #4797)`,
`See sys_migration.verified_at / deviation_observed_at (ADR-0104): deleting bytes cannot be undone, so ` +
`it waits for a verified migration with no deviation on record, while reversible work carries on.`,
);
}
return confirmed;
Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-storage/src/storage-route-ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ export const STORAGE_ROUTE_LEDGER: readonly StorageRouteLedgerEntry[] = [

// ── download ──────────────────────────────────────────────────────────────
{ route: 'GET /api/v1/storage/files/:fileId/url', family: 'download', disposition: 'sdk', client: 'storage.getDownloadUrl',
note: 'JSON { success: true, data: { url } } — the authorization-gated signed-URL resolve the dispatcher ledger points at (#3584); the bare { url } it used to answer moved into the declared envelope in #3689' },
note: 'JSON { success: true, data: { url } } — the authorization-gated signed-URL resolve. The dispatcher ledger points here because this protocol, not a dispatcher route, is the canonical storage surface the SDK speaks; the bare { url } it used to answer moved into the declared envelope' },
{ route: 'GET /api/v1/storage/files/:fileId', family: 'download', disposition: 'server-only',
note: 'stable 302 to the same signed URL. This is the value objectql stamps into file/image field payloads (engine.ts buildFileValue), followed verbatim by <img src>/<a href> — a browser URL, not an SDK call. The SDK resolves via the /url sibling above.' },

// ── local-driver loopback (LocalStorageAdapter presign targets) ───────────
{ route: 'PUT /api/v1/storage/_local/raw/:token', family: 'local-driver', disposition: 'server-only',
note: 'the uploadUrl LocalStorageAdapter mints for its own presign tokens. storage.upload does PUT it, but opaquely — via fetchImpl on whatever uploadUrl came back, exactly as it would an S3 presigned URL. HMAC-token-authorized, adapter-internal, never a named SDK method. Answers { success: true, data: { key } }; the { ok: true, key } it used to answer retired in #3689, `ok` being a private second word for `success`.' },
note: 'the uploadUrl LocalStorageAdapter mints for its own presign tokens. storage.upload does PUT it, but opaquely — via fetchImpl on whatever uploadUrl came back, exactly as it would an S3 presigned URL. HMAC-token-authorized, adapter-internal, never a named SDK method. Answers { success: true, data: { key } }; the { ok: true, key } it used to answer retired when every storage route moved to the declared envelope, `ok` being a private second word for `success`.' },
{ route: 'GET /api/v1/storage/_local/raw/:token', family: 'local-driver', disposition: 'server-only',
note: 'ditto for download: the target of getPresignedDownload/getSignedUrl on the local adapter, handed to the browser as an opaque signed link.' },
];
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('[#14328] array-form triggerType still draws the record-change trigger
/NOT bound|never fire/i,
);
expect(msg, 'steers to the supported single token').toMatch(/record-after-write/);
expect(msg, 'cites the standing decision').toMatch(/#3457/);
expect(msg, 'states the standing decision').toMatch(/multi-event arrays are deferred until/);

// The refusal is a refusal: nothing was armed for this flow.
expect(hooks, 'no lifecycle hook registered for an array-form flow').toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('RecordChangeTrigger', () => {
expect(msg).toMatch(/task_assigned_notify/);
expect(msg).toMatch(/array/i);
expect(msg).toMatch(/record-after-write/);
expect(msg).toMatch(/#3457/);
expect(msg).toMatch(/multi-event arrays are deferred until/);
});

it('keeps the generic unsupported-event warning for a non-array bad token', () => {
Expand Down
Loading
Loading