Pushary
The decision layer for AI agents. Your agent asks, a person approves or denies on their phone.
사용해야 할까요
품질 및 안전성
도구 정의와 프로토콜 준수에 대한 자동 분석을 기반으로 합니다.
컨텍스트 비용
이는 서버의 도구가 모델의 컨텍스트에 로드될 때마다 소비되는 대략적인 토큰 수입니다. 수치가 높을수록 다른 작업에 사용할 수 있는 주의가 줄어듭니다.
설치
원클릭 설치
`claude_desktop_config.json` 파일에 다음을 추가하세요:
{
"mcpServers": {
"pushary": {
"url": "https://pushary.com/api/mcp/mcp"
}
}
}원격 엔드포인트
https://pushary.com/api/mcp/mcpstreamable-httphttps://pushary.com/api/mcp/ssesse할 수 있는 일
도구 목록
도구 (5)
🔴send_notification(title, body, url, iconUrl, imageUrl, ...)
Send a one-way push notification to the user's phone and browser. Nothing is awaited; use ask_user instead when you need an answer back. Reach for this when a long-running task finishes and the user asked to be told, when the agent hits an error it cannot resolve on its own, or for any "notify me when my agent needs me" moment while the user is away from the terminal. By default the notification reaches every device connected to the site; narrow delivery with subscriberIds, externalIds, or tags. The optional context object turns the tap-through into a rich detail page (summary, bullet details, changed files, error info, next steps), and context.askQuestion embeds a decision prompt on that page, returning a linkedCorrelationId you can poll with wait_for_answer. Returns per-channel delivery counts for web and mobile, plus a warning when zero devices are connected. Works from Claude Code, Codex, Cursor, Hermes, or any MCP client; no Claude subscription is required. SIDE EFFECT: delivers real notifications to real devices immediately.
입력 스키마
{
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Notification title shown on the lock screen (max 100 chars). Lead with the outcome, e.g. \"Build finished\" or \"Migration failed\"."
},
"body": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "Notification body text (max 500 chars). One or two sentences the user can act on without opening anything."
},
"url": {
"description": "URL opened when the user taps the notification. Ignored if context is provided, because a context detail page URL is generated automatically.",
"type": "string",
"format": "uri"
},
"iconUrl": {
"description": "URL of the notification icon image",
"type": "string",
"format": "uri"
},
"imageUrl": {
"description": "URL of a large image shown in the notification",
"type": "string",
"format": "uri"
},
"agentName": {
"description": "Name of the agent sending this notification, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification so the user knows which session is talking. Falls back to the MCP client name if omitted.",
"type": "string",
"maxLength": 100
},
"sessionId": {
"description": "Opaque per-session id of the sending agent, so parallel sessions are attributed separately in the activity feed.",
"type": "string",
"maxLength": 128
},
"machineId": {
"description": "Stable machine id of the sending agent, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"subscriberIds": {
"description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.",
"type": "array",
"items": {
"type": "string"
}
},
"externalIds": {
"description": "Deliver only to subscribers matching these external IDs.",
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "Deliver only to subscribers that have any of these tags.",
"type": "array",
"items": {
"type": "string"
}
},
"context": {
"description": "Structured context rendered as a rich detail page when the user taps the notification. Strongly recommended for task_complete and error notifications so the user can act from their phone.",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"task_complete",
"error",
"info"
],
"description": "What kind of update this is. Use \"task_complete\" when work finished, \"error\" when something failed (delivered with high urgency), \"info\" for everything else."
},
"summary": {
"description": "Short summary of what happened, shown at the top of the detail page",
"type": "string"
},
"details": {
"description": "Bullet-point details rendered as a list",
"type": "array",
"items": {
"type": "string"
}
},
"filesChanged": {
"description": "Paths of files that were created or modified",
"type": "array",
"items": {
"type": "string"
}
},
"errorMessage": {
"description": "The error message, when type is \"error\"",
"type": "string"
},
"errorFile": {
"description": "File path where the error occurred",
"type": "string"
},
"nextSteps": {
"description": "What the user should do next, e.g. \"Review the PR\" or \"Re-run with --force\"",
"type": "string"
},
"askQuestion": {
"description": "Embed a decision prompt on the detail page. The response includes a linkedCorrelationId; pass it to wait_for_answer to collect the answer. The embedded question expires 10 minutes after it is created.",
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "A follow-up question shown below the context (e.g. \"Retry with a different approach?\")"
},
"type": {
"default": "confirm",
"description": "Question type: confirm (yes/no), select (pick from options), or input (free text)",
"type": "string",
"enum": [
"confirm",
"select",
"input"
]
},
"options": {
"description": "The 2 to 6 choices for a select question",
"minItems": 2,
"maxItems": 6,
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"question"
]
}
},
"required": [
"type"
]
},
"env": {
"description": "Set to \"test\" from a test suite. The notification is recorded in the activity feed and nothing is delivered to a phone or browser. The X-Pushary-Env: test header does the same for every call on the connection.",
"type": "string",
"enum": [
"test"
]
}
},
"required": [
"title",
"body"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}출력 스키마
{
"type": "object",
"properties": {
"sent": {
"description": "Total devices reached, web plus mobile. Zero is a successful call that found nobody to deliver to, not an error.",
"type": "number"
},
"delivery": {
"description": "Per-channel outcome. The two channels are independent with no cross-fallback, so each reports its own result.",
"type": "object",
"properties": {
"web": {
"type": "object",
"properties": {
"recipients": {
"type": "number",
"description": "Browsers that accepted the push."
},
"status": {
"description": "Why web delivery reached nobody, present only when it reached nobody.",
"type": "string"
}
},
"required": [
"recipients"
],
"additionalProperties": false
},
"mobile": {
"type": "object",
"properties": {
"recipients": {
"type": "number",
"description": "Phones that accepted the push."
},
"status": {
"description": "Why mobile delivery reached nobody, present only when it reached nobody.",
"type": "string"
}
},
"required": [
"recipients"
],
"additionalProperties": false
}
},
"required": [
"web",
"mobile"
],
"additionalProperties": false
},
"warning": {
"description": "Present only when the notification reached zero devices, naming what the user has to connect.",
"type": "string"
},
"linkedCorrelationId": {
"description": "Present only when context.askQuestion embedded a decision prompt. Pass it to wait_for_answer to collect the response.",
"type": "string"
},
"hint": {
"description": "What to do next, when there is a next step.",
"type": "string"
},
"env": {
"description": "Echoed when the call was test traffic.",
"type": "string",
"enum": [
"test"
]
}
},
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
}🔴ask_user(question, type, options, questions, placeholder, ...)
Ask the user a question as a push notification on their phone and block until they answer. Reach for this whenever you need the user's decision and they may be away from the current client: approving a risky or irreversible step (deleting files, force pushing, spending money, sending external messages), picking between implementation options, or supplying missing input. The user answers from the lock screen or a decision page; you do not need a separate wait_for_answer call because this tool waits by default. Three question types: "confirm" (yes/no), "select" (2 to 6 fixed choices), "input" (free text). A single call blocks for at most 55 seconds. If a live question times out, nextAction is "wait_for_answer": poll once with timeoutMs 55000. If that poll is also unanswered, cancel the phone question before asking in the current chat or client. If cancellation returns handoffAction "stop", stop. Otherwise, if cancellation returns false, poll once for 1 second and honor the answer that won the race. Cancelled, expired, and missing questions are reported as terminal states rather than as timeouts. Every response carries answerUrl, the signed-in dashboard page where this question is waiting. When you report that you are waiting, print that URL to the user so they can answer from a browser instead of hunting for it. Works from Claude Code, Codex, Cursor, Hermes, or any MCP client; no Claude subscription is required. SIDE EFFECT: sends a real push notification.
입력 스키마
{
"type": "object",
"properties": {
"question": {
"description": "The question shown on the user's lock screen (500 chars; a longer one is cut). Phrase it so it is answerable at a glance; put background in context instead.",
"type": "string",
"minLength": 1
},
"type": {
"default": "confirm",
"description": "Question type: confirm renders yes/no buttons, select renders the options list, input renders a free-text field.",
"type": "string",
"enum": [
"confirm",
"select",
"input"
]
},
"options": {
"description": "The 2 to 6 choices for a select question. Required when type is \"select\", ignored otherwise. The answered value is the chosen option string.",
"minItems": 2,
"maxItems": 6,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"questions": {
"description": "ONE question, in the richer Claude-compatible shape: a header, per-option descriptions, multiSelect, and an optional write-in. Exactly one keeps already-installed clients answerable; asking several means several calls. Runtime-populated; ordinary callers should omit it and use question/type/options.",
"minItems": 1,
"maxItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"header": {
"type": "string",
"maxLength": 40
},
"multiSelect": {
"type": "boolean"
},
"allowOther": {
"type": "boolean"
},
"options": {
"minItems": 2,
"maxItems": 4,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"description": {
"type": "string",
"maxLength": 500
}
},
"required": [
"label"
]
}
}
},
"required": [
"question",
"multiSelect",
"options"
]
}
},
"placeholder": {
"description": "Hint text shown inside the free-text field for input questions",
"type": "string",
"maxLength": 200
},
"context": {
"description": "One or two sentences about what the agent is working on, shown above the question so the user can decide without opening the terminal.",
"type": "string",
"maxLength": 500
},
"wait": {
"default": true,
"description": "true (default) blocks until the user answers or the timeout fires. Set false to return immediately with a pending correlationId and poll it yourself via wait_for_answer.",
"type": "boolean"
},
"timeoutMs": {
"description": "How long this call blocks, in milliseconds (max 55000). Defaults to the site policy timeout. The question stays open for 10 minutes regardless, so a timeout here is not a refusal; follow up with wait_for_answer.",
"type": "integer",
"minimum": 1000,
"maximum": 55000
},
"callbackUrl": {
"description": "Webhook URL that receives a POST with the answer when the user responds, signed with the X-Pushary-Signature header. Useful when the agent process may exit before the answer arrives.",
"type": "string",
"format": "uri"
},
"agentName": {
"description": "Name of the agent asking, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification title so the user knows which session needs them. Falls back to the MCP client name if omitted. Cut to 100 chars.",
"type": "string",
"minLength": 0
},
"sessionId": {
"description": "Opaque per-session id of the asking agent, so parallel sessions are attributed separately.",
"type": "string",
"maxLength": 128
},
"machineId": {
"description": "Stable machine id of the asking agent, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"toolName": {
"description": "The tool this approval is for (e.g. \"Bash\"), so the user can choose to always-allow it.",
"type": "string",
"maxLength": 100
},
"toolTarget": {
"description": "Compact target of the tool call (e.g. the command head \"git push\" for Bash, or a file extension like \".ts\" for Edit/Write). Used to mine policy suggestions.",
"type": "string",
"maxLength": 80
},
"repoKey": {
"description": "Stable repository identity for the working directory, e.g. \"github.com/acme/api\". Lets an approval routing rule scoped to one repository avoid governing another. Optional; omit it and only workspace-wide routing rules apply.",
"type": "string",
"maxLength": 200
},
"toolPath": {
"description": "MACHINE-POPULATED. Exact absolute Write file_path from the runtime, for diagnostic correlation only. Models must omit it.",
"type": "string",
"maxLength": 4096,
"format": "starts_with",
"pattern": "^\\/.*"
},
"toolUseId": {
"description": "MACHINE-POPULATED. The agent RUNTIME's own identifier for the tool call this approval gates, forwarded verbatim by a hook that received it. Do NOT invent, guess, derive, or reuse a value: two different questions sent under the same id collapse into one, and the second one never reaches a human. If you are a model deciding to call this tool, omit this field.",
"type": "string",
"maxLength": 200
},
"waitEndsAt": {
"description": "MACHINE-POPULATED. When the agent hook stops waiting live and hands control back to the terminal. The question may remain answerable after this time. Ordinary callers should omit it.",
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"sessionToolAware": {
"description": "MACHINE-POPULATED. Set by a Pushary hook whose engine applies a session tool grant with its risky-command ceiling, so the grant is only offered where it takes effect. If you are a model deciding to call this tool, omit this field.",
"type": "boolean"
},
"requestId": {
"description": "MACHINE-POPULATED. The CALLER's own identifier for one logical invocation, used only when the runtime supplies no toolUseId. Mint it once, outside your retry loop, and send the same value on every attempt, so three retries of one ask become one decision. Do NOT derive it from the question text or reuse it across two deliberate asks: both collapse a real second question into the first one's answer. If you are a model deciding to call this tool, omit this field.",
"type": "string",
"maxLength": 200
},
"intent": {
"description": "The user's stated task (from their last prompt), one line. Shown as the Intent line so the user can see why the agent stopped. Cut to 500 chars.",
"type": "string",
"minLength": 0
},
"action": {
"description": "The concrete operation about to happen, one line. Shown as the Action line. Cut to 500 chars.",
"type": "string",
"minLength": 0
},
"blocker": {
"description": "The single gating reason the agent stopped, one line. Shown as the Blocker line. Cut to 500 chars.",
"type": "string",
"minLength": 0
},
"actionBody": {
"description": "The diff (Edit/Write) or full command (Bash/apply_patch), secret-redacted and size-capped. Rendered as a collapsible detail block; never used as the push body.",
"type": "string",
"maxLength": 4000
},
"scopePath": {
"description": "Set ONLY when this approval exists because the path falls outside the scope the user ratified via propose_scope. Approving then widens the run scope to include this exact path, so the user is not asked again for the same area.",
"type": "string",
"maxLength": 200
},
"subscriberIds": {
"description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.",
"type": "array",
"items": {
"type": "string"
}
},
"externalIds": {
"description": "Deliver only to subscribers matching these external IDs.",
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "Deliver only to subscribers that have any of these tags.",
"type": "array",
"items": {
"type": "string"
}
},
"env": {
"description": "Set to \"test\" from a test suite. The question is stored and returned as pending, and nothing is delivered to a phone, browser or Slack. The X-Pushary-Env: test header does the same for every call on the connection.",
"type": "string",
"enum": [
"test"
]
}
},
"required": [
"question"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}출력 스키마
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"description": "Id of the question that was created. Pass it to wait_for_answer to keep waiting, or to cancel_question to retract it."
},
"question": {
"type": "string",
"description": "The question exactly as the user saw it."
},
"type": {
"type": "string",
"enum": [
"confirm",
"select",
"input"
],
"description": "The question type that was rendered."
},
"answered": {
"description": "True once the user responded. Absent on the wait:false path, where nothing was awaited.",
"type": "boolean"
},
"value": {
"description": "The user's answer: \"yes\" or \"no\" for confirm, the chosen option for select, the typed text for input.",
"type": "string"
},
"note": {
"description": "Free text the user added alongside their answer.",
"type": "string"
},
"answerSource": {
"description": "Recorded answering surface, when known. Missing provenance is not proof of a phone answer; sandbox is simulated.",
"type": "string",
"enum": [
"dashboard",
"mobile_app",
"mobile_background",
"decide_page",
"live_page",
"answer_link",
"inbox",
"slack",
"mac_app",
"sandbox"
]
},
"timedOut": {
"description": "True when the initial wait ended while the question was still live. Poll once with wait_for_answer, then follow handoffAction when present, otherwise nextAction.",
"type": "boolean"
},
"nextAction": {
"description": "Backward-compatible next step: poll once or ask in the current client. Follow handoffAction first when present.",
"type": "string",
"enum": [
"wait_for_answer",
"ask_in_current_client"
]
},
"handoffAction": {
"description": "Race-safe directive for updated clients. Takes precedence over nextAction: cancel before asking in the current client, or stop the handoff.",
"type": "string",
"enum": [
"cancel_then_ask_in_current_client",
"stop"
]
},
"status": {
"description": "The question state. Only pending is a live unanswered wait; cancelled, expired, missing, and unavailable must not be described as timeouts.",
"type": "string",
"enum": [
"answered",
"pending",
"cancelled",
"expired",
"missing",
"unavailable",
"notified",
"terminal",
"stopped"
]
},
"deliveryMode": {
"description": "Effective delivery policy for this decision.",
"type": "string",
"enum": [
"push_first",
"push_only",
"notify_only",
"terminal_only"
]
},
"policyTimeoutMs": {
"description": "Policy wait window in milliseconds. Callers must also honor their host deadline.",
"type": "number",
"minimum": 0
},
"mode": {
"description": "The site delivery mode that stopped this call from waiting.",
"type": "string",
"enum": [
"notify_only",
"terminal_only"
]
},
"expiresInSeconds": {
"description": "How long the question stays answerable.",
"type": "number"
},
"delivery": {
"description": "Per-channel reach for the push carrying this question.",
"type": "object",
"properties": {
"web": {
"type": "number",
"description": "Browsers the question reached."
},
"mobile": {
"type": "number",
"description": "Phones the question reached."
},
"pending": {
"description": "True when delivery was still in flight when this returned, so the counts above are not final.",
"type": "boolean"
}
},
"required": [
"web",
"mobile"
],
"additionalProperties": false
},
"suppressed": {
"description": "True when the PHONE push was deliberately held because a terminal on this machine is active. It says nothing about the notch, the dashboard or Slack, which are unaffected and may still be showing this question. A caller with no screen of its own should treat it as the terminal's to answer; a caller that can render the question itself should keep waiting.",
"type": "boolean"
},
"noDevices": {
"description": "True when no phone, browser, or Slack channel could receive the question. Do not wait; follow handoffAction immediately.",
"type": "boolean"
},
"answerUrl": {
"description": "The signed-in dashboard page where this question is waiting. Print it when you tell the user you are waiting, so they can answer from a browser.",
"type": "string"
},
"waitEndsAt": {
"description": "When the agent hook stops waiting live. On an idempotent replay this is the original question's deadline, which the hook must reuse.",
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"warning": {
"description": "Present only when no channel is connected, naming what the user has to connect.",
"type": "string"
},
"hint": {
"description": "What to do next, when there is a next step.",
"type": "string"
},
"held": {
"description": "Present when the question was stored but deliberately not delivered: test traffic, or a permission ask with no toolName and no sessionId.",
"type": "string",
"enum": [
"test_traffic",
"unattributed"
]
},
"env": {
"description": "Echoed when the call was test traffic.",
"type": "string",
"enum": [
"test"
]
}
},
"required": [
"correlationId",
"question",
"type"
],
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
}🔴propose_scope(doneWhen, sessionId, allowedPaths, offLimitsPaths, promises, ...)
Propose what this run will touch and block until the user ratifies it. Call ONCE at the start of a multi-step run, before doing work. The user sees the paths you intend to change, the areas you promise to leave alone, and your definition of done, and approves the whole thing in one tap. After that, editing a file outside the agreed scope stops being auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval, so you are asked once about the boundary rather than repeatedly about each file. Use glob syntax ("src/**", "docs/**"). WHAT IS ENFORCED: only file paths, and only for tool calls that carry one (Edit, Write, MultiEdit). Shell commands, Read, web requests and every MCP tool carry no path, so a contract says nothing about them and they stay governed by the permission policy. A contract with no paths at all therefore gates NOTHING: use promises for a run that touches no files, and read the returned "enforces" rather than assuming ratified:true means something is being checked. A scope can only turn an automatic approval into a question; it never turns a question into an approval. Scope lives for this session only and is never inherited by another run. Returns { correlationId, ratified, answered, value, enforces, contract }. If the first wait times out, poll its correlationId once; a late phone yes ratifies the stored proposal. If that poll is also pending, cancel it before asking in the current chat whether to continue without an enforced scope. If cancellation loses a race, honor the phone answer instead. Never describe a client-only agreement as ratification. SIDE EFFECT: sends a real push notification.
입력 스키마
{
"type": "object",
"properties": {
"doneWhen": {
"type": "string",
"minLength": 1,
"maxLength": 300,
"description": "What \"finished\" means for this run, one or two lines. Carried for the human to judge against; never enforced automatically."
},
"sessionId": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Your per-session id, as your client reports it for THIS run. Required, and it is the key the gate reads the contract back by: a value that matches no live session still returns ratified:true and enforces nothing. Never invent one, and never reuse one from another run."
},
"allowedPaths": {
"description": "Globs you intend to change, e.g. [\"src/**\", \"docs/*.md\"]. File paths only: a word that is not a path (\"hubspot\", \"summer-campaign\") matches no file and makes every edit read as out of scope. A bare directory is expanded for you, so \"docs\" also covers \"docs/**\". Omit or leave empty to propose no path restriction, which the user is told plainly.",
"maxItems": 40,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"offLimitsPaths": {
"description": "Globs you promise not to touch, e.g. [\".env*\", \"infra/**\"]. These win wherever they overlap allowedPaths. A leading \"**/\" needs a directory before it, so \"**/.env*\" is expanded for you to also cover a root \".env\".",
"maxItems": 40,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"promises": {
"description": "Boundaries that are not file paths: recipients, channels, spend limits, systems you will not open. For an agent whose work is not code (marketing, sales, support, operations), this is where the boundary goes. Shown to the user labelled \"Promised, not checked\" and recorded in the ledger, but NEVER enforced, because the gate judges a file path and these have none. Do not put these in allowedPaths.",
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"agentName": {
"description": "Name of the agent asking, format \"{Agent} - {project}\".",
"type": "string",
"maxLength": 100
},
"machineId": {
"description": "Stable machine id, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"timeoutMs": {
"description": "How long this call blocks, in milliseconds (max 55000).",
"type": "integer",
"minimum": 1000,
"maximum": 55000
}
},
"required": [
"doneWhen",
"sessionId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}출력 스키마
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"description": "Id of the scope question. Pass it to wait_for_answer once when the first wait times out."
},
"ratified": {
"type": "boolean",
"description": "True only on an explicit yes. The contract is in force for this session only when this is true; anything else means proceed as if no scope was agreed."
},
"answered": {
"type": "boolean",
"description": "True when the user responded at all. Answered but not ratified means they declined, so ask what scope they want rather than proceeding."
},
"value": {
"description": "The raw answer behind ratified, \"yes\" or \"no\".",
"type": "string"
},
"enforces": {
"type": "array",
"items": {
"type": "string",
"enum": [
"paths"
]
},
"description": "What this contract CONTAINS that can be checked, not a promise about what the gate on this machine will do. An EMPTY array means nothing here is checked automatically: the contract is a recorded promise, and every action stays governed by the permission policy exactly as it was before. Never tell the user a boundary is enforced when this is empty."
},
"enforcementNote": {
"description": "Present only when there is something true to say about the limits of this contract. Repeat it to the user rather than paraphrasing it.",
"type": "string"
},
"hookSeen": {
"description": "Whether any agent hook has actually reported this sessionId. FALSE means the gate will look this contract up under a key that does not exist, so nothing will be checked no matter what ratified says: fix the session id rather than proceeding as if a scope were in force. ABSENT means the check could not run, which is not evidence either way.",
"type": "boolean"
},
"contract": {
"type": "object",
"properties": {
"allowedPaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Globs the run may change, after expansion. Empty means no path restriction was proposed, which the user was told plainly."
},
"offLimitsPaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Globs the run promised not to touch, after expansion. These win wherever they overlap allowedPaths."
},
"doneWhen": {
"type": "string",
"description": "What finished means for this run, as the user saw it."
},
"promises": {
"description": "Non-path boundaries as the user saw them. Recorded, never enforced.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"allowedPaths",
"offLimitsPaths",
"doneWhen"
],
"additionalProperties": false,
"description": "The scope as it is STORED and gated, echoed back so you and the server hold the same contract. Paths are the EXPANDED ones: a bare directory and a leading \"**/\" each gain the variant they would otherwise have missed, so this can contain more entries than you sent. The user was shown the paths you sent, because the added twin says the same thing to a reader; the expansion only changes what the matcher covers, never what it means."
},
"note": {
"description": "Present only when the scope is not in force, saying what to do instead of proceeding.",
"type": "string"
},
"status": {
"description": "The underlying scope-question state.",
"type": "string",
"enum": [
"answered",
"pending",
"cancelled",
"expired",
"missing",
"unavailable",
"notified",
"terminal",
"stopped"
]
},
"nextAction": {
"description": "Poll one live question once; otherwise ask in the current chat whether to continue without an enforced scope.",
"type": "string",
"enum": [
"wait_for_answer",
"ask_in_current_client"
]
},
"handoffAction": {
"description": "Race-safe directive for updated clients. Takes precedence over nextAction.",
"type": "string",
"enum": [
"cancel_then_ask_in_current_client",
"stop"
]
}
},
"required": [
"correlationId",
"ratified",
"answered",
"enforces",
"contract"
],
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
}🟢wait_for_answer(correlationId, timeoutMs)
Poll once for the user's answer to a previously created question: after ask_user times out, after ask_user with wait:false, or with a linkedCorrelationId from send_notification. Each call blocks until the answer arrives or timeoutMs expires (default 30 seconds, max 55). If a live question is still unanswered after this poll, handoffAction is "cancel_then_ask_in_current_client": cancel the phone question before asking in the current chat or client. If cancellation returns handoffAction "stop", stop. Otherwise, if cancellation returns false, poll once for 1 second and honor the answer that won the race. The response distinguishes pending, cancelled, expired, missing, and unavailable states; cancelled and unavailable mean stop rather than re-ask. nextAction retains only its original values for older clients. Returns { answered:true, status:"answered", value } once the user responds.
입력 스키마
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The correlationId from an earlier ask_user response, or the linkedCorrelationId from a send_notification with an embedded askQuestion"
},
"timeoutMs": {
"description": "How long this one poll blocks, in milliseconds (default 30000, max 55000). Follow handoffAction when present, otherwise nextAction.",
"type": "integer",
"minimum": 1000,
"maximum": 55000
}
},
"required": [
"correlationId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}출력 스키마
{
"type": "object",
"properties": {
"answered": {
"type": "boolean",
"description": "True once the user responded. False means follow handoffAction when present, otherwise nextAction; do not guess that every unanswered state is a timeout."
},
"status": {
"type": "string",
"enum": [
"answered",
"pending",
"cancelled",
"expired",
"missing",
"unavailable"
],
"description": "The actual question state. Only pending is a live unanswered wait."
},
"value": {
"description": "The user's answer: \"yes\" or \"no\" for confirm, the chosen option for select, the typed text for input. Present only when answered is true.",
"type": "string"
},
"note": {
"description": "Free text the user added alongside their answer.",
"type": "string"
},
"answerSource": {
"description": "Recorded answering surface, when known. Missing provenance is not proof of a phone answer; sandbox is simulated.",
"type": "string",
"enum": [
"dashboard",
"mobile_app",
"mobile_background",
"decide_page",
"live_page",
"answer_link",
"inbox",
"slack",
"mac_app",
"sandbox"
]
},
"nextAction": {
"description": "Backward-compatible next step for older clients. Follow handoffAction first when present.",
"type": "string",
"enum": [
"wait_for_answer",
"ask_in_current_client"
]
},
"handoffAction": {
"description": "Race-safe directive for updated clients. Takes precedence over nextAction.",
"type": "string",
"enum": [
"cancel_then_ask_in_current_client",
"stop"
]
},
"hint": {
"description": "What to do next, when there is a next step.",
"type": "string"
}
},
"required": [
"answered",
"status"
],
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
}🔴cancel_question(correlationId, handoff)
Retract a pending question so it can no longer be answered. Use this when a question became irrelevant before the user replied: the agent found the answer itself, the task was aborted, or a newer question supersedes it. Cancelling prevents a stale approval from arriving later and acting on work that has moved on. Only affects questions that are still pending; questions expire on their own 10 minutes after creation. Returns { cancelled: true } when a pending question was removed. False means it was already answered, expired, unknown, or unavailable; when status is unavailable, follow handoffAction and stop rather than opening another answer surface.
입력 스키마
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The correlationId of the pending question to cancel, as returned by ask_user or send_notification"
},
"handoff": {
"description": "True when you are cancelling because you are about to ask the same question in the current client. For the next minute the Pushary hook then lets your own question tool through instead of sending it back to the phone. Set automatically whenever a live question is cancelled.",
"type": "boolean"
}
},
"required": [
"correlationId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}출력 스키마
{
"type": "object",
"properties": {
"cancelled": {
"type": "boolean",
"description": "True when a still-pending question was removed. False when it was already terminal, missing, or unavailable; inspect status and handoffAction when present."
},
"correlationId": {
"type": "string",
"description": "The question this result refers to, echoed back."
},
"askHandoff": {
"description": "True when the session was marked as handing off to the current client, so the hook will not re-ask on the phone for the next minute.",
"type": "boolean"
},
"status": {
"description": "Present when Pushary could not safely read or fence the question state.",
"type": "string",
"const": "unavailable"
},
"handoffAction": {
"description": "Stop rather than opening another answer surface when the question state is unavailable.",
"type": "string",
"const": "stop"
},
"hint": {
"description": "What to do when cancellation could not safely inspect the question.",
"type": "string"
}
},
"required": [
"cancelled",
"correlationId"
],
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
}커뮤니티
증거